mcp-scraper 0.35.1 → 0.37.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/README.md +9 -9
- package/dist/bin/api-server.cjs +25071 -19082
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.cjs +51 -7
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +48 -5
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +995 -222
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +8 -8
- package/dist/bin/paa-harvest.cjs +125 -70
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +4 -4
- package/dist/chunk-345BQXZH.js +712 -0
- package/dist/chunk-345BQXZH.js.map +1 -0
- package/dist/chunk-3LWYPAU5.js +7 -0
- package/dist/chunk-3LWYPAU5.js.map +1 -0
- package/dist/{chunk-M2S27J6Z.js → chunk-44HZLHDV.js} +10 -1
- package/dist/chunk-44HZLHDV.js.map +1 -0
- package/dist/{chunk-62DQAWPF.js → chunk-CSCD2HNS.js} +498 -43
- package/dist/chunk-CSCD2HNS.js.map +1 -0
- package/dist/{chunk-ZID3WQID.js → chunk-FQI5PFE7.js} +9 -71
- package/dist/chunk-FQI5PFE7.js.map +1 -0
- package/dist/{chunk-3HBPKR5G.js → chunk-FSAXLDB3.js} +3 -3
- package/dist/chunk-G3P3ZDB4.js +69 -0
- package/dist/chunk-G3P3ZDB4.js.map +1 -0
- package/dist/{chunk-YRGSEY5L.js → chunk-G7KAVJ3F.js} +2 -2
- package/dist/{chunk-YRGSEY5L.js.map → chunk-G7KAVJ3F.js.map} +1 -1
- package/dist/{chunk-NPMW5HUS.js → chunk-JK2FRDAP.js} +930 -244
- package/dist/chunk-JK2FRDAP.js.map +1 -0
- package/dist/{chunk-XVVNKASZ.js → chunk-LOPKN3YL.js} +118 -73
- package/dist/chunk-LOPKN3YL.js.map +1 -0
- package/dist/{chunk-4ZB3X6BQ.js → chunk-MA5JBAUZ.js} +16 -2
- package/dist/{chunk-4ZB3X6BQ.js.map → chunk-MA5JBAUZ.js.map} +1 -1
- package/dist/chunk-PUJFYJXB.js +684 -0
- package/dist/chunk-PUJFYJXB.js.map +1 -0
- package/dist/{chunk-BWXLTWF7.js → chunk-PWPUKR5U.js} +9 -5
- package/dist/chunk-PWPUKR5U.js.map +1 -0
- package/dist/chunk-Q35WZJJK.js +499 -0
- package/dist/chunk-Q35WZJJK.js.map +1 -0
- package/dist/chunk-QZXKQB7Y.js +414 -0
- package/dist/chunk-QZXKQB7Y.js.map +1 -0
- package/dist/{db-YAI5AQOI.js → db-N3YECFWR.js} +12 -2
- package/dist/{extract-bundle-ONWZVV55.js → extract-bundle-346R6MXD.js} +284 -98
- package/dist/extract-bundle-346R6MXD.js.map +1 -0
- package/dist/index.cjs +129 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +4 -4
- package/dist/location-data-repository-O2VII3ON.js +35 -0
- package/dist/{server-GKUTC73B.js → server-QKDDEBVQ.js} +7325 -3762
- package/dist/server-QKDDEBVQ.js.map +1 -0
- package/dist/site-extract-repository-I3VM6WXN.js +62 -0
- package/dist/site-extract-repository-I3VM6WXN.js.map +1 -0
- package/dist/{worker-645BZPEK.js → worker-TTFXPPDK.js} +7 -7
- package/docs/hosted-location-data.md +108 -0
- package/docs/mcp-tool-craft-lint.generated.md +6 -3
- package/docs/mcp-tool-manifest.generated.json +1447 -240
- package/docs/mcp-tool-quality-spec.md +1 -1
- package/docs/specs/connected-services-control-plane-decoupling-spec.md +1044 -0
- package/docs/specs/kernel-stealth-captcha-test-matrix.md +278 -0
- package/docs/specs/multimodal-image-memory-architecture-spec.md +1022 -0
- package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +36 -27
- package/package.json +7 -6
- package/dist/chunk-62DQAWPF.js.map +0 -1
- package/dist/chunk-BWXLTWF7.js.map +0 -1
- package/dist/chunk-M2S27J6Z.js.map +0 -1
- package/dist/chunk-NPMW5HUS.js.map +0 -1
- package/dist/chunk-R7EETU7Z.js +0 -419
- package/dist/chunk-R7EETU7Z.js.map +0 -1
- package/dist/chunk-U44TPRST.js +0 -130
- package/dist/chunk-U44TPRST.js.map +0 -1
- package/dist/chunk-XVVNKASZ.js.map +0 -1
- package/dist/chunk-YR4LJ6AQ.js +0 -7
- package/dist/chunk-YR4LJ6AQ.js.map +0 -1
- package/dist/chunk-YV2FUEBX.js +0 -851
- package/dist/chunk-YV2FUEBX.js.map +0 -1
- package/dist/chunk-ZID3WQID.js.map +0 -1
- package/dist/extract-bundle-ONWZVV55.js.map +0 -1
- package/dist/server-GKUTC73B.js.map +0 -1
- package/dist/site-extract-repository-L6BHWVDU.js +0 -30
- /package/dist/{chunk-3HBPKR5G.js.map → chunk-FSAXLDB3.js.map} +0 -0
- /package/dist/{db-YAI5AQOI.js.map → db-N3YECFWR.js.map} +0 -0
- /package/dist/{site-extract-repository-L6BHWVDU.js.map → location-data-repository-O2VII3ON.js.map} +0 -0
- /package/dist/{worker-645BZPEK.js.map → worker-TTFXPPDK.js.map} +0 -0
package/dist/chunk-R7EETU7Z.js
DELETED
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
// src/api/image-audit.ts
|
|
2
|
-
var OVER_BYTES = 100 * 1024;
|
|
3
|
-
var MODERN = /* @__PURE__ */ new Set(["webp", "avif", "svg", "svg+xml"]);
|
|
4
|
-
function formatBytes(n) {
|
|
5
|
-
if (n == null) return null;
|
|
6
|
-
if (n < 1024) return `${n} B`;
|
|
7
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
8
|
-
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
9
|
-
}
|
|
10
|
-
var decodeEntities = (u) => u.replace(/&/g, "&").replace(/�?38;/g, "&").replace(/&/gi, "&").trim();
|
|
11
|
-
var dedupKey = (u) => u.replace(/^https?:\/\//i, "//").replace(/\/$/, "");
|
|
12
|
-
var formatOf = (ct, url) => {
|
|
13
|
-
if (ct) {
|
|
14
|
-
const m = ct.split(";")[0].trim().toLowerCase();
|
|
15
|
-
if (m.startsWith("image/")) return m.replace("image/", "");
|
|
16
|
-
}
|
|
17
|
-
return (url.split("?")[0].match(/\.([a-z0-9]+)$/i)?.[1] || "unknown").toLowerCase();
|
|
18
|
-
};
|
|
19
|
-
function collectUrls(pages) {
|
|
20
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
21
|
-
for (const p of pages) for (const raw of p.imageLinks || []) {
|
|
22
|
-
const u = decodeEntities(raw);
|
|
23
|
-
if (!/^https?:\/\//i.test(u)) continue;
|
|
24
|
-
if (/[{}]|%7[bd]/i.test(u)) continue;
|
|
25
|
-
const k = dedupKey(u);
|
|
26
|
-
const prev = byKey.get(k);
|
|
27
|
-
if (!prev || /^https:/i.test(u) && /^http:/i.test(prev)) byKey.set(k, u);
|
|
28
|
-
}
|
|
29
|
-
return [...byKey.values()];
|
|
30
|
-
}
|
|
31
|
-
async function sizeAndType(url, timeoutMs) {
|
|
32
|
-
const ctrl = new AbortController();
|
|
33
|
-
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
34
|
-
const read = (res) => ({
|
|
35
|
-
len: res.headers.get("content-length"),
|
|
36
|
-
cr: res.headers.get("content-range"),
|
|
37
|
-
ct: res.headers.get("content-type"),
|
|
38
|
-
status: res.status
|
|
39
|
-
});
|
|
40
|
-
try {
|
|
41
|
-
let bytes = null;
|
|
42
|
-
let ct = null;
|
|
43
|
-
let status = null;
|
|
44
|
-
try {
|
|
45
|
-
const h = read(await fetch(url, { method: "HEAD", redirect: "follow", signal: ctrl.signal }));
|
|
46
|
-
status = h.status;
|
|
47
|
-
ct = h.ct;
|
|
48
|
-
if (h.len) bytes = Number(h.len);
|
|
49
|
-
} catch {
|
|
50
|
-
}
|
|
51
|
-
if (bytes == null) {
|
|
52
|
-
const g = await fetch(url, { method: "GET", headers: { Range: "bytes=0-0" }, redirect: "follow", signal: ctrl.signal });
|
|
53
|
-
const r = read(g);
|
|
54
|
-
status = status ?? r.status;
|
|
55
|
-
ct = ct ?? r.ct;
|
|
56
|
-
if (r.cr && r.cr.includes("/")) {
|
|
57
|
-
const tail = r.cr.split("/")[1];
|
|
58
|
-
if (tail && tail !== "*") bytes = Number(tail);
|
|
59
|
-
} else if (r.len) bytes = Number(r.len);
|
|
60
|
-
try {
|
|
61
|
-
await g.body?.cancel();
|
|
62
|
-
} catch {
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return { url, status, bytes: Number.isFinite(bytes) ? bytes : null, contentType: ct };
|
|
66
|
-
} catch (e) {
|
|
67
|
-
return { url, status: null, bytes: null, contentType: null, error: String(e.message || e) };
|
|
68
|
-
} finally {
|
|
69
|
-
clearTimeout(t);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async function pool(items, n, fn) {
|
|
73
|
-
const out = new Array(items.length);
|
|
74
|
-
let i = 0;
|
|
75
|
-
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
76
|
-
while (i < items.length) {
|
|
77
|
-
const idx = i++;
|
|
78
|
-
out[idx] = await fn(items[idx]);
|
|
79
|
-
}
|
|
80
|
-
}));
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
|
-
async function auditImages(pages, opts = {}) {
|
|
84
|
-
const concurrency = opts.concurrency ?? 12;
|
|
85
|
-
const timeoutMs = opts.timeoutMs ?? 12e3;
|
|
86
|
-
const urls = collectUrls(pages).slice(0, opts.max ?? 5e3);
|
|
87
|
-
const heads = await pool(urls, concurrency, (u) => sizeAndType(u, timeoutMs));
|
|
88
|
-
const rows = heads.map((r) => {
|
|
89
|
-
const format = formatOf(r.contentType, r.url);
|
|
90
|
-
return {
|
|
91
|
-
...r,
|
|
92
|
-
size: formatBytes(r.bytes),
|
|
93
|
-
format,
|
|
94
|
-
over100kb: r.bytes != null && r.bytes > OVER_BYTES,
|
|
95
|
-
legacyFormat: format !== "unknown" && !MODERN.has(format)
|
|
96
|
-
};
|
|
97
|
-
});
|
|
98
|
-
const sized = rows.filter((r) => r.bytes != null);
|
|
99
|
-
const totalBytes = sized.reduce((a, r) => a + r.bytes, 0);
|
|
100
|
-
const formatCounts = {};
|
|
101
|
-
for (const r of rows) formatCounts[r.format] = (formatCounts[r.format] || 0) + 1;
|
|
102
|
-
return {
|
|
103
|
-
rows: rows.sort((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0)),
|
|
104
|
-
summary: {
|
|
105
|
-
unique: rows.length,
|
|
106
|
-
sized: sized.length,
|
|
107
|
-
totalBytes,
|
|
108
|
-
totalSize: formatBytes(totalBytes) ?? "0 B",
|
|
109
|
-
avgSize: formatBytes(Math.round(totalBytes / (sized.length || 1))) ?? "0 B",
|
|
110
|
-
over100kb: rows.filter((r) => r.over100kb).length,
|
|
111
|
-
legacyFormat: rows.filter((r) => r.legacyFormat).length,
|
|
112
|
-
formatCounts
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
function renderImageSection(audit) {
|
|
117
|
-
const s = audit.summary;
|
|
118
|
-
const fmt = Object.entries(s.formatCounts).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(" ");
|
|
119
|
-
const heaviest = audit.rows.filter((r) => r.over100kb).slice(0, 15);
|
|
120
|
-
const lines = [
|
|
121
|
-
`## Images`,
|
|
122
|
-
`**${s.unique} unique images** \xB7 ${s.totalSize} total \xB7 ${s.avgSize} avg \xB7 ${s.over100kb} over 100 KB \xB7 ${s.legacyFormat} legacy format`,
|
|
123
|
-
`Formats: ${fmt}`
|
|
124
|
-
];
|
|
125
|
-
if (heaviest.length) {
|
|
126
|
-
lines.push("", "| Size | Format | URL |", "|------|--------|-----|");
|
|
127
|
-
for (const r of heaviest) lines.push(`| ${r.size} | ${r.format} | ${r.url} |`);
|
|
128
|
-
}
|
|
129
|
-
return lines.join("\n");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/api/seo-link-report.ts
|
|
133
|
-
function registrableDomain(host) {
|
|
134
|
-
const h = host.replace(/^www\./, "").toLowerCase();
|
|
135
|
-
const parts = h.split(".");
|
|
136
|
-
return parts.length <= 2 ? h : parts.slice(-2).join(".");
|
|
137
|
-
}
|
|
138
|
-
function buildLinkReport(edges, metrics, siteUrl) {
|
|
139
|
-
let siteReg = "";
|
|
140
|
-
try {
|
|
141
|
-
siteReg = registrableDomain(new URL(siteUrl).hostname);
|
|
142
|
-
} catch {
|
|
143
|
-
siteReg = "";
|
|
144
|
-
}
|
|
145
|
-
const internalEdges = edges.filter((e) => e.internal);
|
|
146
|
-
const pages = metrics.length;
|
|
147
|
-
const orphans = metrics.filter((m) => m.orphan).length;
|
|
148
|
-
const brokenInternal = internalEdges.filter((e) => e.targetStatus != null && e.targetStatus >= 400).length;
|
|
149
|
-
const sumInlinks = metrics.reduce((a, m) => a + m.inlinks, 0);
|
|
150
|
-
const sumOutlinks = metrics.reduce((a, m) => a + m.outlinksInternal + m.outlinksExternal, 0);
|
|
151
|
-
const distribution = { zero: 0, oneToTwo: 0, threeToTen: 0, elevenPlus: 0 };
|
|
152
|
-
for (const m of metrics) {
|
|
153
|
-
if (m.inlinks === 0) distribution.zero++;
|
|
154
|
-
else if (m.inlinks <= 2) distribution.oneToTwo++;
|
|
155
|
-
else if (m.inlinks <= 10) distribution.threeToTen++;
|
|
156
|
-
else distribution.elevenPlus++;
|
|
157
|
-
}
|
|
158
|
-
const topByInlinks = [...metrics].sort((a, b) => b.inlinks - a.inlinks).slice(0, 20).map((m) => ({ url: m.url, inlinks: m.inlinks, outlinksInternal: m.outlinksInternal, outlinksExternal: m.outlinksExternal }));
|
|
159
|
-
const domMap = /* @__PURE__ */ new Map();
|
|
160
|
-
let externalTotal = 0;
|
|
161
|
-
for (const e of edges) {
|
|
162
|
-
let domain;
|
|
163
|
-
try {
|
|
164
|
-
domain = registrableDomain(new URL(e.to).hostname);
|
|
165
|
-
} catch {
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (!domain || domain === siteReg) continue;
|
|
169
|
-
externalTotal++;
|
|
170
|
-
const d = domMap.get(domain) ?? { links: 0, nofollow: 0, pages: /* @__PURE__ */ new Set() };
|
|
171
|
-
d.links++;
|
|
172
|
-
if (e.nofollow) d.nofollow++;
|
|
173
|
-
d.pages.add(e.from);
|
|
174
|
-
domMap.set(domain, d);
|
|
175
|
-
}
|
|
176
|
-
const externalDomains = [...domMap.entries()].map(([domain, d]) => ({ domain, links: d.links, nofollow: d.nofollow, pages: d.pages.size })).sort((a, b) => b.links - a.links);
|
|
177
|
-
const round = (n) => Math.round(n * 10) / 10;
|
|
178
|
-
return {
|
|
179
|
-
summary: {
|
|
180
|
-
internal: {
|
|
181
|
-
totalLinks: internalEdges.length,
|
|
182
|
-
pages,
|
|
183
|
-
orphans,
|
|
184
|
-
brokenInternal,
|
|
185
|
-
avgInlinks: pages ? round(sumInlinks / pages) : 0,
|
|
186
|
-
avgOutlinks: pages ? round(sumOutlinks / pages) : 0,
|
|
187
|
-
distribution,
|
|
188
|
-
topByInlinks
|
|
189
|
-
},
|
|
190
|
-
external: {
|
|
191
|
-
totalLinks: externalTotal,
|
|
192
|
-
uniqueDomains: externalDomains.length,
|
|
193
|
-
topDomains: externalDomains.slice(0, 20)
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
externalDomains
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
function renderLinkReport(r) {
|
|
200
|
-
const s = r.summary;
|
|
201
|
-
const lines = [
|
|
202
|
-
`## Link analysis`,
|
|
203
|
-
`**Internal:** ${s.internal.totalLinks} links \xB7 ${s.internal.pages} pages \xB7 avg ${s.internal.avgInlinks} inlinks/page \xB7 ${s.internal.orphans} orphans \xB7 ${s.internal.brokenInternal} broken`,
|
|
204
|
-
`**External:** ${s.external.totalLinks} links to ${s.external.uniqueDomains} domains`,
|
|
205
|
-
``,
|
|
206
|
-
`### Inlink distribution`,
|
|
207
|
-
`- 0 inlinks (orphans): ${s.internal.distribution.zero}`,
|
|
208
|
-
`- 1\u20132: ${s.internal.distribution.oneToTwo}`,
|
|
209
|
-
`- 3\u201310: ${s.internal.distribution.threeToTen}`,
|
|
210
|
-
`- 11+: ${s.internal.distribution.elevenPlus}`,
|
|
211
|
-
``,
|
|
212
|
-
`### Top 20 internal pages by inlinks`,
|
|
213
|
-
`| inlinks | out (int/ext) | URL |`,
|
|
214
|
-
`|---|---|---|`,
|
|
215
|
-
...s.internal.topByInlinks.map((p) => `| ${p.inlinks} | ${p.outlinksInternal}/${p.outlinksExternal} | ${p.url} |`),
|
|
216
|
-
``,
|
|
217
|
-
`### Top 20 external domains by links`,
|
|
218
|
-
`| links | nofollow | from pages | domain |`,
|
|
219
|
-
`|---|---|---|---|`,
|
|
220
|
-
...s.external.topDomains.map((d) => `| ${d.links} | ${d.nofollow} | ${d.pages} | ${d.domain} |`)
|
|
221
|
-
];
|
|
222
|
-
return lines.join("\n");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// src/api/seo-issues.ts
|
|
226
|
-
var THIN_WORDS = 200;
|
|
227
|
-
var TITLE_MAX = 60;
|
|
228
|
-
var TITLE_PX_MAX = 561;
|
|
229
|
-
var TITLE_MIN = 30;
|
|
230
|
-
var META_MAX = 155;
|
|
231
|
-
var META_MIN = 70;
|
|
232
|
-
var H1_MAX = 70;
|
|
233
|
-
var URL_MAX = 115;
|
|
234
|
-
function dupes(pages, key) {
|
|
235
|
-
const groups = /* @__PURE__ */ new Map();
|
|
236
|
-
for (const p of pages) {
|
|
237
|
-
const k = key(p);
|
|
238
|
-
if (k === null || k === void 0 || k === "") continue;
|
|
239
|
-
const ks = String(k);
|
|
240
|
-
if (!groups.has(ks)) groups.set(ks, []);
|
|
241
|
-
groups.get(ks).push(p.url);
|
|
242
|
-
}
|
|
243
|
-
return [...groups.values()].filter((g) => g.length > 1).flat();
|
|
244
|
-
}
|
|
245
|
-
function computeIssues(pages, metrics, precomputedBrokenLinkPages) {
|
|
246
|
-
const group = (urls) => ({ count: urls.length, urls: urls.slice(0, 200) });
|
|
247
|
-
const where = (fn) => pages.filter(fn).map((p) => p.url);
|
|
248
|
-
const ok = (p) => p.status === 200;
|
|
249
|
-
const pathname = (u) => {
|
|
250
|
-
try {
|
|
251
|
-
return new URL(u).pathname;
|
|
252
|
-
} catch {
|
|
253
|
-
return "";
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
const report = {
|
|
257
|
-
"title.missing": group(where((p) => ok(p) && !p.title)),
|
|
258
|
-
"title.duplicate": group(dupes(pages.filter(ok), (p) => p.title)),
|
|
259
|
-
"title.tooLong": group(where((p) => (p.titleLength ?? 0) > TITLE_MAX || (p.titlePixels ?? 0) > TITLE_PX_MAX)),
|
|
260
|
-
"title.tooShort": group(where((p) => p.title != null && (p.titleLength ?? 0) < TITLE_MIN)),
|
|
261
|
-
"title.sameAsH1": group(where((p) => !!p.title && !!p.h1 && p.title.trim() === p.h1.trim())),
|
|
262
|
-
"meta.missing": group(where((p) => ok(p) && !p.metaDescription)),
|
|
263
|
-
"meta.duplicate": group(dupes(pages.filter(ok), (p) => p.metaDescription)),
|
|
264
|
-
"meta.tooLong": group(where((p) => (p.metaDescLength ?? 0) > META_MAX)),
|
|
265
|
-
"meta.tooShort": group(where((p) => p.metaDescription != null && (p.metaDescLength ?? 0) < META_MIN)),
|
|
266
|
-
"h1.missing": group(where((p) => ok(p) && !p.h1)),
|
|
267
|
-
"h1.multiple": group(where((p) => !!p.h1_2)),
|
|
268
|
-
"h1.duplicate": group(dupes(pages.filter(ok), (p) => p.h1)),
|
|
269
|
-
"h1.tooLong": group(where((p) => (p.h1?.length ?? 0) > H1_MAX)),
|
|
270
|
-
"h2.missing": group(where((p) => ok(p) && p.h2Count === 0)),
|
|
271
|
-
"indexability.nonIndexable": group(where((p) => !p.indexable)),
|
|
272
|
-
"indexability.noindex": group(where((p) => p.indexabilityReason === "noindex")),
|
|
273
|
-
"canonical.missing": group(where((p) => ok(p) && !p.canonicalUrl)),
|
|
274
|
-
"canonical.canonicalised": group(where((p) => p.indexabilityReason === "canonicalised")),
|
|
275
|
-
"response.broken4xx": group(where((p) => (p.status ?? 0) >= 400 && (p.status ?? 0) < 500)),
|
|
276
|
-
"response.error5xx": group(where((p) => (p.status ?? 0) >= 500)),
|
|
277
|
-
"response.redirect3xx": group(where((p) => (p.status ?? 0) >= 300 && (p.status ?? 0) < 400)),
|
|
278
|
-
"response.noResponse": group(where((p) => p.status === null)),
|
|
279
|
-
"content.thin": group(where((p) => ok(p) && p.wordCount > 0 && p.wordCount < THIN_WORDS)),
|
|
280
|
-
"content.exactDuplicate": group(dupes(pages.filter((p) => ok(p) && p.contentHash), (p) => p.contentHash)),
|
|
281
|
-
"images.missingAlt": group(where((p) => p.imagesMissingAlt > 0)),
|
|
282
|
-
"schema.missing": group(where((p) => ok(p) && p.schemaTypes.length === 0)),
|
|
283
|
-
"url.tooLong": group(where((p) => p.url.length > URL_MAX)),
|
|
284
|
-
"url.uppercase": group(where((p) => /[A-Z]/.test(pathname(p.url)))),
|
|
285
|
-
"url.underscores": group(where((p) => pathname(p.url).includes("_"))),
|
|
286
|
-
"links.orphan": group([...metrics.values()].filter((m) => m.orphan).map((m) => m.url))
|
|
287
|
-
};
|
|
288
|
-
let brokenLinkPages = precomputedBrokenLinkPages;
|
|
289
|
-
if (!brokenLinkPages) {
|
|
290
|
-
const normUrl = (u) => {
|
|
291
|
-
try {
|
|
292
|
-
const x = new URL(u);
|
|
293
|
-
x.hash = "";
|
|
294
|
-
return (x.origin + x.pathname).replace(/\/+$/, "") + x.search;
|
|
295
|
-
} catch {
|
|
296
|
-
return u.replace(/#.*$/, "").replace(/\/+$/, "");
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
const statusByUrl = new Map(pages.map((p) => [normUrl(p.url), p.status]));
|
|
300
|
-
brokenLinkPages = /* @__PURE__ */ new Set();
|
|
301
|
-
for (const p of pages) {
|
|
302
|
-
for (const l of p.outlinks ?? []) {
|
|
303
|
-
if (!l.internal) continue;
|
|
304
|
-
const st = statusByUrl.get(normUrl(l.href));
|
|
305
|
-
if (st != null && st >= 400) brokenLinkPages.add(p.url);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
report["links.brokenInternal"] = group([...brokenLinkPages]);
|
|
310
|
-
return report;
|
|
311
|
-
}
|
|
312
|
-
function renderIssueReport(siteUrl, pages, report, metrics) {
|
|
313
|
-
const total = pages.length;
|
|
314
|
-
const sev = (key) => key.startsWith("response.broken") || key.startsWith("response.error") || key.startsWith("links.broken") ? "\u{1F534}" : key.startsWith("title.missing") || key.startsWith("h1.missing") || key.startsWith("indexability") || key.startsWith("canonical.missing") ? "\u{1F7E0}" : "\u{1F7E1}";
|
|
315
|
-
const rows = Object.entries(report).filter(([, g]) => g.count > 0).sort((a, b) => b[1].count - a[1].count).map(([k, g]) => `| ${sev(k)} | \`${k}\` | ${g.count} | ${g.urls.slice(0, 3).join(" \xB7 ")}${g.urls.length > 3 ? " \u2026" : ""} |`);
|
|
316
|
-
const depths = [...metrics.values()].map((m) => m.crawlDepth).filter((d) => d != null);
|
|
317
|
-
const maxDepth = depths.length ? Math.max(...depths) : 0;
|
|
318
|
-
const orphans = [...metrics.values()].filter((m) => m.orphan).length;
|
|
319
|
-
const topLinked = [...metrics.values()].sort((a, b) => b.inlinks - a.inlinks).slice(0, 5);
|
|
320
|
-
return [
|
|
321
|
-
`# SEO Crawl Report: ${siteUrl}`,
|
|
322
|
-
`**${total} pages crawled** \xB7 max crawl depth ${maxDepth} \xB7 ${orphans} orphan page(s)`,
|
|
323
|
-
`
|
|
324
|
-
## Issues
|
|
325
|
-
| | Issue | Count | Examples |
|
|
326
|
-
|---|-------|-------|----------|
|
|
327
|
-
${rows.join("\n") || "| \u2705 | none | 0 | \u2014 |"}`,
|
|
328
|
-
`
|
|
329
|
-
## Most-linked pages
|
|
330
|
-
${topLinked.map((m) => `- ${m.inlinks} inlinks \xB7 depth ${m.crawlDepth ?? "\u2014"} \xB7 ${m.url}`).join("\n")}`,
|
|
331
|
-
`
|
|
332
|
-
_Thresholds: title ${TITLE_MAX}ch/${TITLE_PX_MAX}px, meta ${META_MAX}ch, H1 ${H1_MAX}ch, thin <${THIN_WORDS} words, URL ${URL_MAX}ch. Pixel widths are estimates._`
|
|
333
|
-
].join("\n");
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// src/api/blob-store.ts
|
|
337
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
338
|
-
import { homedir } from "os";
|
|
339
|
-
import { join, dirname } from "path";
|
|
340
|
-
function byteLength(data) {
|
|
341
|
-
return Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
|
|
342
|
-
}
|
|
343
|
-
var LocalBlobStore = class {
|
|
344
|
-
constructor(baseDir) {
|
|
345
|
-
this.baseDir = baseDir;
|
|
346
|
-
}
|
|
347
|
-
baseDir;
|
|
348
|
-
kind = "local";
|
|
349
|
-
async put(key, data, contentType = "application/octet-stream") {
|
|
350
|
-
const path = join(this.baseDir, "blobs", key);
|
|
351
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
352
|
-
writeFileSync(path, data);
|
|
353
|
-
return { key, url: `file://${path}`, bytes: byteLength(data), contentType };
|
|
354
|
-
}
|
|
355
|
-
async get(key) {
|
|
356
|
-
const path = join(this.baseDir, "blobs", key);
|
|
357
|
-
try {
|
|
358
|
-
const { readFileSync } = await import("fs");
|
|
359
|
-
return readFileSync(path);
|
|
360
|
-
} catch {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
};
|
|
365
|
-
function localBaseDir() {
|
|
366
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join(homedir(), "Downloads", "mcp-scraper");
|
|
367
|
-
}
|
|
368
|
-
var cached = null;
|
|
369
|
-
function getBlobStore() {
|
|
370
|
-
if (cached) return cached;
|
|
371
|
-
if (process.env.BLOB_READ_WRITE_TOKEN) {
|
|
372
|
-
cached = new VercelBlobStore(process.env.BLOB_READ_WRITE_TOKEN);
|
|
373
|
-
} else {
|
|
374
|
-
cached = new LocalBlobStore(localBaseDir());
|
|
375
|
-
}
|
|
376
|
-
return cached;
|
|
377
|
-
}
|
|
378
|
-
var VercelBlobStore = class {
|
|
379
|
-
constructor(token) {
|
|
380
|
-
this.token = token;
|
|
381
|
-
}
|
|
382
|
-
token;
|
|
383
|
-
kind = "vercel-blob";
|
|
384
|
-
async put(key, data, contentType = "application/octet-stream") {
|
|
385
|
-
const { put } = await import("@vercel/blob");
|
|
386
|
-
const body = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
387
|
-
const result = await put(key, body, {
|
|
388
|
-
access: "public",
|
|
389
|
-
token: this.token,
|
|
390
|
-
contentType,
|
|
391
|
-
addRandomSuffix: true
|
|
392
|
-
});
|
|
393
|
-
return { key, url: result.url, bytes: byteLength(data), contentType };
|
|
394
|
-
}
|
|
395
|
-
async get(key) {
|
|
396
|
-
try {
|
|
397
|
-
const { list } = await import("@vercel/blob");
|
|
398
|
-
const res = await list({ prefix: key, token: this.token, limit: 1 });
|
|
399
|
-
const match = res.blobs.find((b) => b.pathname === key || b.pathname.startsWith(key));
|
|
400
|
-
if (!match) return null;
|
|
401
|
-
const resp = await fetch(match.url);
|
|
402
|
-
if (!resp.ok) return null;
|
|
403
|
-
return Buffer.from(await resp.arrayBuffer());
|
|
404
|
-
} catch {
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
export {
|
|
411
|
-
auditImages,
|
|
412
|
-
renderImageSection,
|
|
413
|
-
buildLinkReport,
|
|
414
|
-
renderLinkReport,
|
|
415
|
-
computeIssues,
|
|
416
|
-
renderIssueReport,
|
|
417
|
-
getBlobStore
|
|
418
|
-
};
|
|
419
|
-
//# sourceMappingURL=chunk-R7EETU7Z.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/image-audit.ts","../src/api/seo-link-report.ts","../src/api/seo-issues.ts","../src/api/blob-store.ts"],"sourcesContent":["import type { PageData } from './site-extractor.js'\n\nexport interface ImageRow {\n url: string\n status: number | null\n bytes: number | null\n size: string | null\n contentType: string | null\n format: string\n over100kb: boolean\n legacyFormat: boolean\n error?: string\n}\n\nexport interface ImageAuditSummary {\n unique: number\n sized: number\n totalBytes: number\n totalSize: string\n avgSize: string\n over100kb: number\n legacyFormat: number\n formatCounts: Record<string, number>\n}\n\nexport interface ImageAudit {\n rows: ImageRow[]\n summary: ImageAuditSummary\n}\n\nconst OVER_BYTES = 100 * 1024\nconst MODERN = new Set(['webp', 'avif', 'svg', 'svg+xml'])\n\nexport function formatBytes(n: number | null): string | null {\n if (n == null) return null\n if (n < 1024) return `${n} B`\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`\n return `${(n / (1024 * 1024)).toFixed(2)} MB`\n}\n\nconst decodeEntities = (u: string): string =>\n u.replace(/&/g, '&').replace(/�?38;/g, '&').replace(/&/gi, '&').trim()\n\nconst dedupKey = (u: string): string => u.replace(/^https?:\\/\\//i, '//').replace(/\\/$/, '')\n\nconst formatOf = (ct: string | null, url: string): string => {\n if (ct) { const m = ct.split(';')[0].trim().toLowerCase(); if (m.startsWith('image/')) return m.replace('image/', '') }\n return (url.split('?')[0].match(/\\.([a-z0-9]+)$/i)?.[1] || 'unknown').toLowerCase()\n}\n\nfunction collectUrls(pages: PageData[]): string[] {\n const byKey = new Map<string, string>()\n for (const p of pages) for (const raw of (p.imageLinks || [])) {\n const u = decodeEntities(raw)\n if (!/^https?:\\/\\//i.test(u)) continue\n if (/[{}]|%7[bd]/i.test(u)) continue\n const k = dedupKey(u)\n const prev = byKey.get(k)\n if (!prev || (/^https:/i.test(u) && /^http:/i.test(prev))) byKey.set(k, u)\n }\n return [...byKey.values()]\n}\n\nasync function sizeAndType(url: string, timeoutMs: number): Promise<Omit<ImageRow, 'size' | 'format' | 'over100kb' | 'legacyFormat'>> {\n const ctrl = new AbortController()\n const t = setTimeout(() => ctrl.abort(), timeoutMs)\n const read = (res: Response) => ({\n len: res.headers.get('content-length'),\n cr: res.headers.get('content-range'),\n ct: res.headers.get('content-type'),\n status: res.status,\n })\n try {\n let bytes: number | null = null\n let ct: string | null = null\n let status: number | null = null\n try {\n const h = read(await fetch(url, { method: 'HEAD', redirect: 'follow', signal: ctrl.signal }))\n status = h.status; ct = h.ct\n if (h.len) bytes = Number(h.len)\n } catch { /* fall through to ranged GET */ }\n if (bytes == null) {\n const g = await fetch(url, { method: 'GET', headers: { Range: 'bytes=0-0' }, redirect: 'follow', signal: ctrl.signal })\n const r = read(g)\n status = status ?? r.status; ct = ct ?? r.ct\n if (r.cr && r.cr.includes('/')) { const tail = r.cr.split('/')[1]; if (tail && tail !== '*') bytes = Number(tail) }\n else if (r.len) bytes = Number(r.len)\n try { await g.body?.cancel() } catch { /* ignore */ }\n }\n return { url, status, bytes: Number.isFinite(bytes as number) ? bytes : null, contentType: ct }\n } catch (e) {\n return { url, status: null, bytes: null, contentType: null, error: String((e as Error).message || e) }\n } finally {\n clearTimeout(t)\n }\n}\n\nasync function pool<T, R>(items: T[], n: number, fn: (item: T) => Promise<R>): Promise<R[]> {\n const out = new Array<R>(items.length)\n let i = 0\n await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {\n while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx]) }\n }))\n return out\n}\n\nexport async function auditImages(\n pages: PageData[],\n opts: { concurrency?: number; timeoutMs?: number; max?: number } = {},\n): Promise<ImageAudit> {\n const concurrency = opts.concurrency ?? 12\n const timeoutMs = opts.timeoutMs ?? 12_000\n const urls = collectUrls(pages).slice(0, opts.max ?? 5000)\n\n const heads = await pool(urls, concurrency, (u) => sizeAndType(u, timeoutMs))\n const rows: ImageRow[] = heads.map(r => {\n const format = formatOf(r.contentType, r.url)\n return {\n ...r,\n size: formatBytes(r.bytes),\n format,\n over100kb: r.bytes != null && r.bytes > OVER_BYTES,\n legacyFormat: format !== 'unknown' && !MODERN.has(format),\n }\n })\n\n const sized = rows.filter(r => r.bytes != null)\n const totalBytes = sized.reduce((a, r) => a + (r.bytes as number), 0)\n const formatCounts: Record<string, number> = {}\n for (const r of rows) formatCounts[r.format] = (formatCounts[r.format] || 0) + 1\n\n return {\n rows: rows.sort((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0)),\n summary: {\n unique: rows.length,\n sized: sized.length,\n totalBytes,\n totalSize: formatBytes(totalBytes) ?? '0 B',\n avgSize: formatBytes(Math.round(totalBytes / (sized.length || 1))) ?? '0 B',\n over100kb: rows.filter(r => r.over100kb).length,\n legacyFormat: rows.filter(r => r.legacyFormat).length,\n formatCounts,\n },\n }\n}\n\nexport function renderImageSection(audit: ImageAudit): string {\n const s = audit.summary\n const fmt = Object.entries(s.formatCounts).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(' ')\n const heaviest = audit.rows.filter(r => r.over100kb).slice(0, 15)\n const lines = [\n `## Images`,\n `**${s.unique} unique images** · ${s.totalSize} total · ${s.avgSize} avg · ${s.over100kb} over 100 KB · ${s.legacyFormat} legacy format`,\n `Formats: ${fmt}`,\n ]\n if (heaviest.length) {\n lines.push('', '| Size | Format | URL |', '|------|--------|-----|')\n for (const r of heaviest) lines.push(`| ${r.size} | ${r.format} | ${r.url} |`)\n }\n return lines.join('\\n')\n}\n","import type { LinkEdge, PageLinkMetrics } from './seo-link-graph.js'\n\nexport interface ExtDomainRow { domain: string; links: number; nofollow: number; pages: number }\n\nexport interface LinkReportSummary {\n internal: {\n totalLinks: number\n pages: number\n orphans: number\n brokenInternal: number\n avgInlinks: number\n avgOutlinks: number\n distribution: { zero: number; oneToTwo: number; threeToTen: number; elevenPlus: number }\n topByInlinks: Array<{ url: string; inlinks: number; outlinksInternal: number; outlinksExternal: number }>\n }\n external: {\n totalLinks: number\n uniqueDomains: number\n topDomains: ExtDomainRow[]\n }\n}\n\nexport interface LinkReport {\n summary: LinkReportSummary\n externalDomains: ExtDomainRow[]\n}\n\nfunction registrableDomain(host: string): string {\n const h = host.replace(/^www\\./, '').toLowerCase()\n const parts = h.split('.')\n return parts.length <= 2 ? h : parts.slice(-2).join('.')\n}\n\nexport function buildLinkReport(edges: LinkEdge[], metrics: PageLinkMetrics[], siteUrl: string): LinkReport {\n let siteReg = ''\n try { siteReg = registrableDomain(new URL(siteUrl).hostname) } catch { siteReg = '' }\n const internalEdges = edges.filter(e => e.internal)\n const pages = metrics.length\n const orphans = metrics.filter(m => m.orphan).length\n const brokenInternal = internalEdges.filter(e => e.targetStatus != null && e.targetStatus >= 400).length\n const sumInlinks = metrics.reduce((a, m) => a + m.inlinks, 0)\n const sumOutlinks = metrics.reduce((a, m) => a + m.outlinksInternal + m.outlinksExternal, 0)\n\n const distribution = { zero: 0, oneToTwo: 0, threeToTen: 0, elevenPlus: 0 }\n for (const m of metrics) {\n if (m.inlinks === 0) distribution.zero++\n else if (m.inlinks <= 2) distribution.oneToTwo++\n else if (m.inlinks <= 10) distribution.threeToTen++\n else distribution.elevenPlus++\n }\n\n const topByInlinks = [...metrics].sort((a, b) => b.inlinks - a.inlinks).slice(0, 20)\n .map(m => ({ url: m.url, inlinks: m.inlinks, outlinksInternal: m.outlinksInternal, outlinksExternal: m.outlinksExternal }))\n\n const domMap = new Map<string, { links: number; nofollow: number; pages: Set<string> }>()\n let externalTotal = 0\n for (const e of edges) {\n let domain: string\n try { domain = registrableDomain(new URL(e.to).hostname) } catch { continue }\n if (!domain || domain === siteReg) continue\n externalTotal++\n const d = domMap.get(domain) ?? { links: 0, nofollow: 0, pages: new Set<string>() }\n d.links++\n if (e.nofollow) d.nofollow++\n d.pages.add(e.from)\n domMap.set(domain, d)\n }\n const externalDomains: ExtDomainRow[] = [...domMap.entries()]\n .map(([domain, d]) => ({ domain, links: d.links, nofollow: d.nofollow, pages: d.pages.size }))\n .sort((a, b) => b.links - a.links)\n\n const round = (n: number) => Math.round(n * 10) / 10\n return {\n summary: {\n internal: {\n totalLinks: internalEdges.length,\n pages, orphans, brokenInternal,\n avgInlinks: pages ? round(sumInlinks / pages) : 0,\n avgOutlinks: pages ? round(sumOutlinks / pages) : 0,\n distribution,\n topByInlinks,\n },\n external: {\n totalLinks: externalTotal,\n uniqueDomains: externalDomains.length,\n topDomains: externalDomains.slice(0, 20),\n },\n },\n externalDomains,\n }\n}\n\nexport function renderLinkReport(r: LinkReport): string {\n const s = r.summary\n const lines = [\n `## Link analysis`,\n `**Internal:** ${s.internal.totalLinks} links · ${s.internal.pages} pages · avg ${s.internal.avgInlinks} inlinks/page · ${s.internal.orphans} orphans · ${s.internal.brokenInternal} broken`,\n `**External:** ${s.external.totalLinks} links to ${s.external.uniqueDomains} domains`,\n ``,\n `### Inlink distribution`,\n `- 0 inlinks (orphans): ${s.internal.distribution.zero}`,\n `- 1–2: ${s.internal.distribution.oneToTwo}`,\n `- 3–10: ${s.internal.distribution.threeToTen}`,\n `- 11+: ${s.internal.distribution.elevenPlus}`,\n ``,\n `### Top 20 internal pages by inlinks`,\n `| inlinks | out (int/ext) | URL |`,\n `|---|---|---|`,\n ...s.internal.topByInlinks.map(p => `| ${p.inlinks} | ${p.outlinksInternal}/${p.outlinksExternal} | ${p.url} |`),\n ``,\n `### Top 20 external domains by links`,\n `| links | nofollow | from pages | domain |`,\n `|---|---|---|---|`,\n ...s.external.topDomains.map(d => `| ${d.links} | ${d.nofollow} | ${d.pages} | ${d.domain} |`),\n ]\n return lines.join('\\n')\n}\n","import type { PageData } from './site-extractor.js'\nimport type { PageLinkMetrics } from './seo-link-graph.js'\n\nexport interface IssueGroup { count: number; urls: string[] }\nexport type IssueReport = Record<string, IssueGroup>\n\nconst THIN_WORDS = 200\nconst TITLE_MAX = 60\nconst TITLE_PX_MAX = 561\nconst TITLE_MIN = 30\nconst META_MAX = 155\nconst META_MIN = 70\nconst H1_MAX = 70\nconst URL_MAX = 115\n\nfunction dupes<T>(pages: PageData[], key: (p: PageData) => T | null | undefined): string[] {\n const groups = new Map<string, string[]>()\n for (const p of pages) {\n const k = key(p)\n if (k === null || k === undefined || k === '') continue\n const ks = String(k)\n if (!groups.has(ks)) groups.set(ks, [])\n groups.get(ks)!.push(p.url)\n }\n return [...groups.values()].filter(g => g.length > 1).flat()\n}\n\nexport function computeIssues(pages: PageData[], metrics: Map<string, PageLinkMetrics>, precomputedBrokenLinkPages?: Set<string>): IssueReport {\n const group = (urls: string[]): IssueGroup => ({ count: urls.length, urls: urls.slice(0, 200) })\n const where = (fn: (p: PageData) => boolean) => pages.filter(fn).map(p => p.url)\n const ok = (p: PageData) => p.status === 200\n const pathname = (u: string) => { try { return new URL(u).pathname } catch { return '' } }\n\n const report: IssueReport = {\n 'title.missing': group(where(p => ok(p) && !p.title)),\n 'title.duplicate': group(dupes(pages.filter(ok), p => p.title)),\n 'title.tooLong': group(where(p => (p.titleLength ?? 0) > TITLE_MAX || (p.titlePixels ?? 0) > TITLE_PX_MAX)),\n 'title.tooShort': group(where(p => p.title != null && (p.titleLength ?? 0) < TITLE_MIN)),\n 'title.sameAsH1': group(where(p => !!p.title && !!p.h1 && p.title.trim() === p.h1.trim())),\n 'meta.missing': group(where(p => ok(p) && !p.metaDescription)),\n 'meta.duplicate': group(dupes(pages.filter(ok), p => p.metaDescription)),\n 'meta.tooLong': group(where(p => (p.metaDescLength ?? 0) > META_MAX)),\n 'meta.tooShort': group(where(p => p.metaDescription != null && (p.metaDescLength ?? 0) < META_MIN)),\n 'h1.missing': group(where(p => ok(p) && !p.h1)),\n 'h1.multiple': group(where(p => !!p.h1_2)),\n 'h1.duplicate': group(dupes(pages.filter(ok), p => p.h1)),\n 'h1.tooLong': group(where(p => (p.h1?.length ?? 0) > H1_MAX)),\n 'h2.missing': group(where(p => ok(p) && p.h2Count === 0)),\n 'indexability.nonIndexable': group(where(p => !p.indexable)),\n 'indexability.noindex': group(where(p => p.indexabilityReason === 'noindex')),\n 'canonical.missing': group(where(p => ok(p) && !p.canonicalUrl)),\n 'canonical.canonicalised': group(where(p => p.indexabilityReason === 'canonicalised')),\n 'response.broken4xx': group(where(p => (p.status ?? 0) >= 400 && (p.status ?? 0) < 500)),\n 'response.error5xx': group(where(p => (p.status ?? 0) >= 500)),\n 'response.redirect3xx': group(where(p => (p.status ?? 0) >= 300 && (p.status ?? 0) < 400)),\n 'response.noResponse': group(where(p => p.status === null)),\n 'content.thin': group(where(p => ok(p) && p.wordCount > 0 && p.wordCount < THIN_WORDS)),\n 'content.exactDuplicate': group(dupes(pages.filter(p => ok(p) && p.contentHash), p => p.contentHash)),\n 'images.missingAlt': group(where(p => p.imagesMissingAlt > 0)),\n 'schema.missing': group(where(p => ok(p) && p.schemaTypes.length === 0)),\n 'url.tooLong': group(where(p => p.url.length > URL_MAX)),\n 'url.uppercase': group(where(p => /[A-Z]/.test(pathname(p.url)))),\n 'url.underscores': group(where(p => pathname(p.url).includes('_'))),\n 'links.orphan': group([...metrics.values()].filter(m => m.orphan).map(m => m.url)),\n }\n\n let brokenLinkPages = precomputedBrokenLinkPages\n if (!brokenLinkPages) {\n const normUrl = (u: string) => { try { const x = new URL(u); x.hash = ''; return (x.origin + x.pathname).replace(/\\/+$/, '') + x.search } catch { return u.replace(/#.*$/, '').replace(/\\/+$/, '') } }\n const statusByUrl = new Map(pages.map(p => [normUrl(p.url), p.status]))\n brokenLinkPages = new Set<string>()\n for (const p of pages) {\n for (const l of p.outlinks ?? []) {\n if (!l.internal) continue\n const st = statusByUrl.get(normUrl(l.href))\n if (st != null && st >= 400) brokenLinkPages.add(p.url)\n }\n }\n }\n report['links.brokenInternal'] = group([...brokenLinkPages])\n\n return report\n}\n\nexport function renderIssueReport(siteUrl: string, pages: PageData[], report: IssueReport, metrics: Map<string, PageLinkMetrics>): string {\n const total = pages.length\n const sev = (key: string): string => (\n key.startsWith('response.broken') || key.startsWith('response.error') || key.startsWith('links.broken') ? '🔴'\n : key.startsWith('title.missing') || key.startsWith('h1.missing') || key.startsWith('indexability') || key.startsWith('canonical.missing') ? '🟠'\n : '🟡'\n )\n const rows = Object.entries(report)\n .filter(([, g]) => g.count > 0)\n .sort((a, b) => b[1].count - a[1].count)\n .map(([k, g]) => `| ${sev(k)} | \\`${k}\\` | ${g.count} | ${g.urls.slice(0, 3).join(' · ')}${g.urls.length > 3 ? ' …' : ''} |`)\n\n const depths = [...metrics.values()].map(m => m.crawlDepth).filter((d): d is number => d != null)\n const maxDepth = depths.length ? Math.max(...depths) : 0\n const orphans = [...metrics.values()].filter(m => m.orphan).length\n const topLinked = [...metrics.values()].sort((a, b) => b.inlinks - a.inlinks).slice(0, 5)\n\n return [\n `# SEO Crawl Report: ${siteUrl}`,\n `**${total} pages crawled** · max crawl depth ${maxDepth} · ${orphans} orphan page(s)`,\n `\\n## Issues\\n| | Issue | Count | Examples |\\n|---|-------|-------|----------|\\n${rows.join('\\n') || '| ✅ | none | 0 | — |'}`,\n `\\n## Most-linked pages\\n${topLinked.map(m => `- ${m.inlinks} inlinks · depth ${m.crawlDepth ?? '—'} · ${m.url}`).join('\\n')}`,\n `\\n_Thresholds: title ${TITLE_MAX}ch/${TITLE_PX_MAX}px, meta ${META_MAX}ch, H1 ${H1_MAX}ch, thin <${THIN_WORDS} words, URL ${URL_MAX}ch. Pixel widths are estimates._`,\n ].join('\\n')\n}\n","import { mkdirSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join, dirname } from 'node:path'\n\nexport interface StoredArtifact {\n key: string\n url: string\n bytes: number\n contentType: string\n}\n\nexport interface BlobStore {\n readonly kind: 'local' | 'vercel-blob'\n put(key: string, data: Buffer | string, contentType?: string): Promise<StoredArtifact>\n get(key: string): Promise<Buffer | null>\n}\n\nfunction byteLength(data: Buffer | string): number {\n return Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data)\n}\n\nexport class LocalBlobStore implements BlobStore {\n readonly kind = 'local' as const\n constructor(private readonly baseDir: string) {}\n\n async put(key: string, data: Buffer | string, contentType = 'application/octet-stream'): Promise<StoredArtifact> {\n const path = join(this.baseDir, 'blobs', key)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, data)\n return { key, url: `file://${path}`, bytes: byteLength(data), contentType }\n }\n\n async get(key: string): Promise<Buffer | null> {\n const path = join(this.baseDir, 'blobs', key)\n try {\n const { readFileSync } = await import('node:fs')\n return readFileSync(path)\n } catch {\n return null\n }\n }\n}\n\nfunction localBaseDir(): string {\n return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join(homedir(), 'Downloads', 'mcp-scraper')\n}\n\nlet cached: BlobStore | null = null\n\nexport function getBlobStore(): BlobStore {\n if (cached) return cached\n if (process.env.BLOB_READ_WRITE_TOKEN) {\n cached = new VercelBlobStore(process.env.BLOB_READ_WRITE_TOKEN)\n } else {\n cached = new LocalBlobStore(localBaseDir())\n }\n return cached\n}\n\nexport function resetBlobStoreCache(): void {\n cached = null\n}\n\nexport class VercelBlobStore implements BlobStore {\n readonly kind = 'vercel-blob' as const\n constructor(private readonly token: string) {}\n\n async put(key: string, data: Buffer | string, contentType = 'application/octet-stream'): Promise<StoredArtifact> {\n const { put } = await import('@vercel/blob')\n const body = Buffer.isBuffer(data) ? data : Buffer.from(data)\n const result = await put(key, body, {\n access: 'public',\n token: this.token,\n contentType,\n addRandomSuffix: true,\n })\n return { key, url: result.url, bytes: byteLength(data), contentType }\n }\n\n async get(key: string): Promise<Buffer | null> {\n try {\n const { list } = await import('@vercel/blob')\n const res = await list({ prefix: key, token: this.token, limit: 1 })\n const match = res.blobs.find(b => b.pathname === key || b.pathname.startsWith(key))\n if (!match) return null\n const resp = await fetch(match.url)\n if (!resp.ok) return null\n return Buffer.from(await resp.arrayBuffer())\n } catch {\n return null\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-U44TPRST.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LedgerOperation
|
|
3
|
-
} from "./chunk-BWXLTWF7.js";
|
|
4
|
-
import {
|
|
5
|
-
sanitizeVendorName
|
|
6
|
-
} from "./chunk-M2S27J6Z.js";
|
|
7
|
-
import {
|
|
8
|
-
getDb
|
|
9
|
-
} from "./chunk-62DQAWPF.js";
|
|
10
|
-
|
|
11
|
-
// src/api/site-extract-repository.ts
|
|
12
|
-
function rowToJob(r) {
|
|
13
|
-
return {
|
|
14
|
-
id: String(r.id),
|
|
15
|
-
userId: r.user_id != null ? Number(r.user_id) : null,
|
|
16
|
-
status: r.status != null ? String(r.status) : "pending",
|
|
17
|
-
startUrl: String(r.start_url ?? ""),
|
|
18
|
-
options: r.options ? JSON.parse(String(r.options)) : {},
|
|
19
|
-
totalUrls: Number(r.total_urls ?? 0),
|
|
20
|
-
doneUrls: Number(r.done_urls ?? 0),
|
|
21
|
-
artifacts: r.artifacts ? JSON.parse(String(r.artifacts)) : null,
|
|
22
|
-
error: r.error != null ? String(r.error) : null,
|
|
23
|
-
billedMc: r.billed_mc != null ? Number(r.billed_mc) : null,
|
|
24
|
-
createdAt: String(r.created_at ?? ""),
|
|
25
|
-
updatedAt: String(r.updated_at ?? "")
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
async function createExtractJob(jobId, userId, startUrl, options) {
|
|
29
|
-
const db = getDb();
|
|
30
|
-
await db.execute({
|
|
31
|
-
sql: `INSERT INTO site_extract_jobs (id, user_id, status, start_url, options, created_at, updated_at)
|
|
32
|
-
VALUES (?, ?, 'pending', ?, ?, datetime('now'), datetime('now'))`,
|
|
33
|
-
args: [jobId, userId, startUrl, JSON.stringify(options)]
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
async function getExtractJob(jobId) {
|
|
37
|
-
const db = getDb();
|
|
38
|
-
const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE id = ?`, args: [jobId] });
|
|
39
|
-
return res.rows[0] ? rowToJob(res.rows[0]) : null;
|
|
40
|
-
}
|
|
41
|
-
async function listExtractJobs(userId) {
|
|
42
|
-
const db = getDb();
|
|
43
|
-
const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE user_id = ? ORDER BY created_at DESC LIMIT 50`, args: [userId] });
|
|
44
|
-
return res.rows.map((r) => rowToJob(r));
|
|
45
|
-
}
|
|
46
|
-
async function setExtractJobTotal(jobId, totalUrls) {
|
|
47
|
-
const db = getDb();
|
|
48
|
-
await db.execute({
|
|
49
|
-
sql: `UPDATE site_extract_jobs SET status = 'running', total_urls = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
50
|
-
args: [totalUrls, jobId]
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
async function saveExtractPages(jobId, pages) {
|
|
54
|
-
if (pages.length === 0) return;
|
|
55
|
-
const db = getDb();
|
|
56
|
-
await db.batch(
|
|
57
|
-
pages.map((p) => ({
|
|
58
|
-
sql: `INSERT OR REPLACE INTO site_extract_pages (job_id, url, page) VALUES (?, ?, ?)`,
|
|
59
|
-
args: [jobId, p.url, JSON.stringify({ ...p, schema: [] })]
|
|
60
|
-
}))
|
|
61
|
-
);
|
|
62
|
-
await db.execute({
|
|
63
|
-
sql: `UPDATE site_extract_jobs
|
|
64
|
-
SET done_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?), updated_at = datetime('now')
|
|
65
|
-
WHERE id = ?`,
|
|
66
|
-
args: [jobId, jobId]
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
async function getExtractedPages(jobId) {
|
|
70
|
-
const db = getDb();
|
|
71
|
-
const res = await db.execute({ sql: `SELECT page FROM site_extract_pages WHERE job_id = ?`, args: [jobId] });
|
|
72
|
-
return res.rows.map((r) => JSON.parse(String(r.page)));
|
|
73
|
-
}
|
|
74
|
-
async function countSuccessfulPages(jobId) {
|
|
75
|
-
const db = getDb();
|
|
76
|
-
const res = await db.execute({
|
|
77
|
-
sql: `SELECT COUNT(*) AS n FROM site_extract_pages
|
|
78
|
-
WHERE job_id = ? AND json_extract(page, '$.status') = 200 AND json_extract(page, '$.wordCount') > 0`,
|
|
79
|
-
args: [jobId]
|
|
80
|
-
});
|
|
81
|
-
return Number(res.rows[0]?.n ?? 0);
|
|
82
|
-
}
|
|
83
|
-
async function getExtractedUrls(jobId) {
|
|
84
|
-
const db = getDb();
|
|
85
|
-
const res = await db.execute({ sql: `SELECT url FROM site_extract_pages WHERE job_id = ?`, args: [jobId] });
|
|
86
|
-
return new Set(res.rows.map((r) => String(r.url)));
|
|
87
|
-
}
|
|
88
|
-
async function completeExtractJob(jobId, artifacts) {
|
|
89
|
-
const db = getDb();
|
|
90
|
-
await db.execute({
|
|
91
|
-
sql: `UPDATE site_extract_jobs SET status = 'complete', artifacts = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
92
|
-
args: [JSON.stringify(artifacts ?? []), jobId]
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
async function failExtractJob(jobId, error) {
|
|
96
|
-
const db = getDb();
|
|
97
|
-
await db.execute({
|
|
98
|
-
sql: `UPDATE site_extract_jobs SET status = 'failed', error = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
99
|
-
args: [sanitizeVendorName(error).slice(0, 2e3), jobId]
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
async function settleExtractJob(jobId, userId, refundMc, netChargeMc, reference) {
|
|
103
|
-
const db = getDb();
|
|
104
|
-
const job = await getExtractJob(jobId);
|
|
105
|
-
if (!job || job.billedMc != null) return;
|
|
106
|
-
if (refundMc <= 0) {
|
|
107
|
-
await db.execute({ sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`, args: [Math.max(0, netChargeMc), jobId] });
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
await db.batch([
|
|
111
|
-
{ sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?", args: [refundMc, userId] },
|
|
112
|
-
{ sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, ?, ?)", args: [userId, refundMc, LedgerOperation.EXTRACT_SITE_REFUND, reference] },
|
|
113
|
-
{ sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`, args: [Math.max(0, netChargeMc), jobId] }
|
|
114
|
-
]);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export {
|
|
118
|
-
createExtractJob,
|
|
119
|
-
getExtractJob,
|
|
120
|
-
listExtractJobs,
|
|
121
|
-
setExtractJobTotal,
|
|
122
|
-
saveExtractPages,
|
|
123
|
-
getExtractedPages,
|
|
124
|
-
countSuccessfulPages,
|
|
125
|
-
getExtractedUrls,
|
|
126
|
-
completeExtractJob,
|
|
127
|
-
failExtractJob,
|
|
128
|
-
settleExtractJob
|
|
129
|
-
};
|
|
130
|
-
//# sourceMappingURL=chunk-U44TPRST.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/site-extract-repository.ts"],"sourcesContent":["import { getDb } from './db.js'\nimport { sanitizeVendorName } from '../errors.js'\nimport { LedgerOperation } from './rates.js'\nimport type { PageData } from './site-extractor.js'\n\nexport type ExtractJobStatus = 'pending' | 'running' | 'complete' | 'failed'\n\nexport interface ExtractJobRow {\n id: string\n userId: number | null\n status: ExtractJobStatus\n startUrl: string\n options: Record<string, unknown>\n totalUrls: number\n doneUrls: number\n artifacts: Array<{ key: string; url: string; bytes: number; contentType: string }> | null\n error: string | null\n billedMc: number | null\n createdAt: string\n updatedAt: string\n}\n\nfunction rowToJob(r: Record<string, unknown>): ExtractJobRow {\n return {\n id: String(r.id),\n userId: r.user_id != null ? Number(r.user_id) : null,\n status: (r.status != null ? String(r.status) : 'pending') as ExtractJobStatus,\n startUrl: String(r.start_url ?? ''),\n options: r.options ? JSON.parse(String(r.options)) : {},\n totalUrls: Number(r.total_urls ?? 0),\n doneUrls: Number(r.done_urls ?? 0),\n artifacts: r.artifacts ? JSON.parse(String(r.artifacts)) : null,\n error: r.error != null ? String(r.error) : null,\n billedMc: r.billed_mc != null ? Number(r.billed_mc) : null,\n createdAt: String(r.created_at ?? ''),\n updatedAt: String(r.updated_at ?? ''),\n }\n}\n\nexport async function createExtractJob(jobId: string, userId: number, startUrl: string, options: Record<string, unknown>): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `INSERT INTO site_extract_jobs (id, user_id, status, start_url, options, created_at, updated_at)\n VALUES (?, ?, 'pending', ?, ?, datetime('now'), datetime('now'))`,\n args: [jobId, userId, startUrl, JSON.stringify(options)],\n })\n}\n\nexport async function getExtractJob(jobId: string): Promise<ExtractJobRow | null> {\n const db = getDb()\n const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE id = ?`, args: [jobId] })\n return res.rows[0] ? rowToJob(res.rows[0] as unknown as Record<string, unknown>) : null\n}\n\nexport async function listExtractJobs(userId: number): Promise<ExtractJobRow[]> {\n const db = getDb()\n const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE user_id = ? ORDER BY created_at DESC LIMIT 50`, args: [userId] })\n return res.rows.map(r => rowToJob(r as unknown as Record<string, unknown>))\n}\n\nexport async function setExtractJobTotal(jobId: string, totalUrls: number): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs SET status = 'running', total_urls = ?, updated_at = datetime('now') WHERE id = ?`,\n args: [totalUrls, jobId],\n })\n}\n\nexport async function saveExtractPages(jobId: string, pages: PageData[]): Promise<void> {\n if (pages.length === 0) return\n const db = getDb()\n await db.batch(\n pages.map(p => ({\n sql: `INSERT OR REPLACE INTO site_extract_pages (job_id, url, page) VALUES (?, ?, ?)`,\n args: [jobId, p.url, JSON.stringify({ ...p, schema: [] })],\n })),\n )\n await db.execute({\n sql: `UPDATE site_extract_jobs\n SET done_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?), updated_at = datetime('now')\n WHERE id = ?`,\n args: [jobId, jobId],\n })\n}\n\nexport async function getExtractedPages(jobId: string): Promise<PageData[]> {\n const db = getDb()\n const res = await db.execute({ sql: `SELECT page FROM site_extract_pages WHERE job_id = ?`, args: [jobId] })\n return res.rows.map(r => JSON.parse(String((r as unknown as Record<string, unknown>).page)) as PageData)\n}\n\nexport async function countSuccessfulPages(jobId: string): Promise<number> {\n const db = getDb()\n const res = await db.execute({\n sql: `SELECT COUNT(*) AS n FROM site_extract_pages\n WHERE job_id = ? AND json_extract(page, '$.status') = 200 AND json_extract(page, '$.wordCount') > 0`,\n args: [jobId],\n })\n return Number((res.rows[0] as unknown as Record<string, unknown>)?.n ?? 0)\n}\n\nexport async function getExtractedUrls(jobId: string): Promise<Set<string>> {\n const db = getDb()\n const res = await db.execute({ sql: `SELECT url FROM site_extract_pages WHERE job_id = ?`, args: [jobId] })\n return new Set(res.rows.map(r => String((r as unknown as Record<string, unknown>).url)))\n}\n\nexport async function completeExtractJob(jobId: string, artifacts: ExtractJobRow['artifacts']): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs SET status = 'complete', artifacts = ?, updated_at = datetime('now') WHERE id = ?`,\n args: [JSON.stringify(artifacts ?? []), jobId],\n })\n}\n\nexport async function failExtractJob(jobId: string, error: string): Promise<void> {\n const db = getDb()\n await db.execute({\n sql: `UPDATE site_extract_jobs SET status = 'failed', error = ?, updated_at = datetime('now') WHERE id = ?`,\n args: [sanitizeVendorName(error).slice(0, 2000), jobId],\n })\n}\n\nexport async function settleExtractJob(jobId: string, userId: number, refundMc: number, netChargeMc: number, reference: string): Promise<void> {\n const db = getDb()\n const job = await getExtractJob(jobId)\n if (!job || job.billedMc != null) return\n if (refundMc <= 0) {\n await db.execute({ sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`, args: [Math.max(0, netChargeMc), jobId] })\n return\n }\n await db.batch([\n { sql: 'UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?', args: [refundMc, userId] },\n { sql: 'INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, ?, ?)', args: [userId, refundMc, LedgerOperation.EXTRACT_SITE_REFUND, reference] },\n { sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`, args: [Math.max(0, netChargeMc), jobId] },\n ])\n}\n"],"mappings":"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","names":[]}
|