nfunc-mcp 0.3.0 → 0.5.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 +121 -379
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mappers/a11yDedupe.js +38 -7
- package/dist/mappers/a11yDedupe.js.map +1 -1
- package/dist/mappers/defectFormatter.d.ts +9 -1
- package/dist/mappers/defectFormatter.js +53 -13
- package/dist/mappers/defectFormatter.js.map +1 -1
- package/dist/mappers/labFieldComparator.d.ts +62 -0
- package/dist/mappers/labFieldComparator.js +134 -0
- package/dist/mappers/labFieldComparator.js.map +1 -0
- package/dist/mappers/priorityMapper.d.ts +42 -0
- package/dist/mappers/priorityMapper.js +58 -0
- package/dist/mappers/priorityMapper.js.map +1 -1
- package/dist/mappers/psiAggregator.d.ts +130 -0
- package/dist/mappers/psiAggregator.js +293 -0
- package/dist/mappers/psiAggregator.js.map +1 -0
- package/dist/mappers/runComparator.d.ts +85 -0
- package/dist/mappers/runComparator.js +165 -0
- package/dist/mappers/runComparator.js.map +1 -0
- package/dist/mappers/wcagLevels.d.ts +73 -0
- package/dist/mappers/wcagLevels.js +320 -0
- package/dist/mappers/wcagLevels.js.map +1 -0
- package/dist/mappers/webVitalsMapper.d.ts +52 -0
- package/dist/mappers/webVitalsMapper.js +131 -0
- package/dist/mappers/webVitalsMapper.js.map +1 -0
- package/dist/tools/accessibility.d.ts +1 -0
- package/dist/tools/accessibility.js +488 -63
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/lighthouse.js +370 -102
- package/dist/tools/lighthouse.js.map +1 -1
- package/dist/tools/performanceAudit.d.ts +2 -0
- package/dist/tools/performanceAudit.js +446 -0
- package/dist/tools/performanceAudit.js.map +1 -0
- package/dist/tools/performanceAuditPlan.d.ts +2 -0
- package/dist/tools/performanceAuditPlan.js +438 -0
- package/dist/tools/performanceAuditPlan.js.map +1 -0
- package/dist/utils/batchState.d.ts +75 -0
- package/dist/utils/batchState.js +128 -0
- package/dist/utils/batchState.js.map +1 -0
- package/dist/utils/csvReader.d.ts +20 -0
- package/dist/utils/csvReader.js +172 -0
- package/dist/utils/csvReader.js.map +1 -0
- package/dist/utils/httpClient.d.ts +84 -0
- package/dist/utils/httpClient.js +171 -0
- package/dist/utils/httpClient.js.map +1 -0
- package/dist/utils/outputParsers.js +26 -30
- package/dist/utils/outputParsers.js.map +1 -1
- package/dist/utils/psiAuth.d.ts +26 -0
- package/dist/utils/psiAuth.js +36 -0
- package/dist/utils/psiAuth.js.map +1 -0
- package/dist/utils/psiParser.d.ts +135 -0
- package/dist/utils/psiParser.js +200 -0
- package/dist/utils/psiParser.js.map +1 -0
- package/dist/utils/publicUrl.d.ts +17 -0
- package/dist/utils/publicUrl.js +115 -0
- package/dist/utils/publicUrl.js.map +1 -0
- package/dist/utils/sitemapReader.d.ts +27 -0
- package/dist/utils/sitemapReader.js +272 -0
- package/dist/utils/sitemapReader.js.map +1 -0
- package/dist/utils/urlClassifier.d.ts +45 -0
- package/dist/utils/urlClassifier.js +267 -0
- package/dist/utils/urlClassifier.js.map +1 -0
- package/dist/utils/urlInput.d.ts +30 -0
- package/dist/utils/urlInput.js +130 -0
- package/dist/utils/urlInput.js.map +1 -0
- package/docs/manual.md +769 -0
- package/docs/psi-report-spec.md +174 -0
- package/package.json +13 -3
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap discovery.
|
|
3
|
+
*
|
|
4
|
+
* The cheapest honest way to learn which pages a site has. A sitemap is
|
|
5
|
+
* authoritative (the site published it), instant (one or two requests), and
|
|
6
|
+
* needs no crawling, no robots.txt politeness budget and no HTML parsing. A
|
|
7
|
+
* crawler is the fallback for sites without one, and is deliberately not built
|
|
8
|
+
* yet — everything downstream works without it.
|
|
9
|
+
*
|
|
10
|
+
* Sitemaps are parsed with regular expressions rather than an XML library.
|
|
11
|
+
* That is normally a mistake, but a sitemap is a machine-generated document
|
|
12
|
+
* with a fixed two-element vocabulary, and the alternative is a dependency
|
|
13
|
+
* carried by every install of this server for one code path. The parser reads
|
|
14
|
+
* <loc> only and ignores everything else, so malformed markup degrades to
|
|
15
|
+
* fewer URLs rather than to wrong ones.
|
|
16
|
+
*/
|
|
17
|
+
import { gunzipSync } from "node:zlib";
|
|
18
|
+
import { httpGet } from "./httpClient.js";
|
|
19
|
+
/** A sitemap index can point at hundreds of children; walking all of them is rarely worth it. */
|
|
20
|
+
const MAX_SITEMAPS = 25;
|
|
21
|
+
const MAX_URLS = 50_000;
|
|
22
|
+
const FETCH_TIMEOUT_MS = 20_000;
|
|
23
|
+
const LOC_RE = /<loc>\s*([^<\s][^<]*?)\s*<\/loc>/gi;
|
|
24
|
+
function decodeXmlEntities(value) {
|
|
25
|
+
return value
|
|
26
|
+
.replace(/</g, "<")
|
|
27
|
+
.replace(/>/g, ">")
|
|
28
|
+
.replace(/"/g, '"')
|
|
29
|
+
.replace(/'/g, "'")
|
|
30
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCharCode(Number(d)))
|
|
31
|
+
// Ampersand last, so "&lt;" does not become "<".
|
|
32
|
+
.replace(/&/g, "&");
|
|
33
|
+
}
|
|
34
|
+
function extractLocs(xml) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const match of xml.matchAll(LOC_RE)) {
|
|
37
|
+
const loc = decodeXmlEntities(match[1].trim());
|
|
38
|
+
if (loc)
|
|
39
|
+
out.push(loc);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** A <sitemapindex> points at more sitemaps; a <urlset> holds pages. */
|
|
44
|
+
function isSitemapIndex(xml) {
|
|
45
|
+
return /<sitemapindex[\s>]/i.test(xml);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Sitemap locations declared in robots.txt.
|
|
49
|
+
*
|
|
50
|
+
* Large sites frequently do not serve /sitemap.xml and only announce the real
|
|
51
|
+
* location here, so checking robots first turns a "no sitemap found" dead end
|
|
52
|
+
* into a hit. It is also the polite thing to read before touching a site.
|
|
53
|
+
*/
|
|
54
|
+
async function sitemapsFromRobots(origin) {
|
|
55
|
+
const robotsUrl = new URL("/robots.txt", origin).toString();
|
|
56
|
+
const result = await httpGet(robotsUrl, { timeoutMs: 10_000, retries: 1 });
|
|
57
|
+
if (!result.ok)
|
|
58
|
+
return [];
|
|
59
|
+
const found = [];
|
|
60
|
+
for (const line of result.body.split(/\r?\n/)) {
|
|
61
|
+
const match = /^\s*sitemap:\s*(\S+)/i.exec(line);
|
|
62
|
+
if (match)
|
|
63
|
+
found.push(match[1]);
|
|
64
|
+
}
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Non-HTML entries a performance audit should never spend a PSI call on.
|
|
69
|
+
* Image and video sitemaps are common and would otherwise fill the sample.
|
|
70
|
+
*/
|
|
71
|
+
function looksAuditable(url) {
|
|
72
|
+
return !/\.(jpe?g|png|gif|webp|avif|svg|ico|pdf|zip|gz|mp4|webm|mp3|xml|json|txt|css|js)(\?|$)/i.test(url);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Sitemap locations worth guessing, beyond the two standard ones.
|
|
76
|
+
*
|
|
77
|
+
* Ordered by how often they pay off. These only run when the standard
|
|
78
|
+
* locations and robots.txt have produced nothing, so the common case still
|
|
79
|
+
* costs one or two requests — a 404 from Google's edge is cheap, but seven of
|
|
80
|
+
* them on every audit would not be.
|
|
81
|
+
*/
|
|
82
|
+
const CANDIDATE_PATHS = [
|
|
83
|
+
"/sitemap-index.xml",
|
|
84
|
+
"/sitemap/sitemap.xml",
|
|
85
|
+
"/sitemap/index.xml",
|
|
86
|
+
"/wp-sitemap.xml", // WordPress 5.5+
|
|
87
|
+
"/sitemap_index.xml.gz",
|
|
88
|
+
"/sitemap1.xml",
|
|
89
|
+
"/sitemap.txt", // plain text, one URL per line
|
|
90
|
+
];
|
|
91
|
+
/**
|
|
92
|
+
* `<link rel="sitemap">` in the homepage head.
|
|
93
|
+
*
|
|
94
|
+
* Rare but authoritative when present, and it costs one request we can often
|
|
95
|
+
* justify anyway. Only consulted after the cheaper guesses fail.
|
|
96
|
+
*/
|
|
97
|
+
async function sitemapFromHomepageLink(origin) {
|
|
98
|
+
const result = await httpGet(origin, { timeoutMs: 15_000, retries: 0 });
|
|
99
|
+
if (!result.ok)
|
|
100
|
+
return [];
|
|
101
|
+
const found = [];
|
|
102
|
+
for (const match of result.body.matchAll(/<link\b[^>]*>/gi)) {
|
|
103
|
+
const tag = match[0];
|
|
104
|
+
if (!/rel=["']?sitemap["']?/i.test(tag))
|
|
105
|
+
continue;
|
|
106
|
+
const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
|
|
107
|
+
if (href) {
|
|
108
|
+
try {
|
|
109
|
+
found.push(new URL(href, origin).toString());
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Unparseable href — ignore rather than fail discovery over it.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A sitemap.txt is a bare newline-delimited URL list, not XML. Detected by
|
|
120
|
+
* content rather than extension, because servers are inconsistent about both.
|
|
121
|
+
*/
|
|
122
|
+
function isPlainTextSitemap(body) {
|
|
123
|
+
const head = body.trimStart().slice(0, 200);
|
|
124
|
+
return !head.startsWith("<") && /^https?:\/\//im.test(head);
|
|
125
|
+
}
|
|
126
|
+
function extractPlainTextUrls(body) {
|
|
127
|
+
return body
|
|
128
|
+
.split(/\r?\n/)
|
|
129
|
+
.map((line) => line.trim())
|
|
130
|
+
.filter((line) => /^https?:\/\//i.test(line));
|
|
131
|
+
}
|
|
132
|
+
export async function readSitemap(origin) {
|
|
133
|
+
const warnings = [];
|
|
134
|
+
const sitemapsRead = [];
|
|
135
|
+
const attempted = [];
|
|
136
|
+
const urls = new Set();
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
let truncated = false;
|
|
139
|
+
/**
|
|
140
|
+
* Tiers, cheapest and most authoritative first. Each is tried only if every
|
|
141
|
+
* earlier one came up empty, so a site that declares its sitemap properly
|
|
142
|
+
* still costs two requests while a site that hides it gets a real search.
|
|
143
|
+
*/
|
|
144
|
+
const declared = await sitemapsFromRobots(origin);
|
|
145
|
+
const tiers = [
|
|
146
|
+
{ name: "robots.txt", locations: async () => declared },
|
|
147
|
+
{
|
|
148
|
+
name: "standard locations",
|
|
149
|
+
locations: async () => [
|
|
150
|
+
new URL("/sitemap.xml", origin).toString(),
|
|
151
|
+
new URL("/sitemap_index.xml", origin).toString(),
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{ name: "homepage <link rel=sitemap>", locations: () => sitemapFromHomepageLink(origin) },
|
|
155
|
+
{
|
|
156
|
+
name: "common CMS locations",
|
|
157
|
+
locations: async () => CANDIDATE_PATHS.map((path) => new URL(path, origin).toString()),
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
for (const tier of tiers) {
|
|
161
|
+
// Stop on the first tier that finds a sitemap *document*, not the first
|
|
162
|
+
// that yields URLs. web.dev serves a valid index whose children time out;
|
|
163
|
+
// falling through on an empty result sent discovery guessing at nine more
|
|
164
|
+
// locations for 43s when the sitemap had already been located.
|
|
165
|
+
if (sitemapsRead.length > 0)
|
|
166
|
+
break;
|
|
167
|
+
const queue = await tier.locations();
|
|
168
|
+
let consecutiveFailures = 0;
|
|
169
|
+
while (queue.length > 0) {
|
|
170
|
+
if (sitemapsRead.length >= MAX_SITEMAPS || urls.size >= MAX_URLS) {
|
|
171
|
+
truncated = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
const next = queue.shift();
|
|
175
|
+
if (seen.has(next))
|
|
176
|
+
continue;
|
|
177
|
+
seen.add(next);
|
|
178
|
+
attempted.push(next);
|
|
179
|
+
// Large sites commonly serve .xml.gz. It arrives as an opaque gzip payload
|
|
180
|
+
// rather than with Content-Encoding, so fetch does not inflate it and a
|
|
181
|
+
// text read produces binary noise — hence the explicit binary path.
|
|
182
|
+
const gzipped = /\.gz(\?|$)/i.test(next);
|
|
183
|
+
// No retry: discovery must stay fast, and a sitemap that times out once
|
|
184
|
+
// usually times out again. Retrying doubled the cost of the slow case
|
|
185
|
+
// for no observed benefit.
|
|
186
|
+
const result = await httpGet(next, {
|
|
187
|
+
timeoutMs: FETCH_TIMEOUT_MS,
|
|
188
|
+
retries: 0,
|
|
189
|
+
raw: gzipped,
|
|
190
|
+
});
|
|
191
|
+
if (!result.ok) {
|
|
192
|
+
// A 404 or 403 on a guessed location is the expected answer to "is it
|
|
193
|
+
// here?" — cheap, informative, and no reason to stop guessing. Only
|
|
194
|
+
// expensive failures (timeouts, 5xx) count toward giving up.
|
|
195
|
+
if (result.status === 404 || result.status === 403)
|
|
196
|
+
continue;
|
|
197
|
+
consecutiveFailures++;
|
|
198
|
+
// A sitemap index can list hundreds of children. If the first few all
|
|
199
|
+
// time out, the rest almost certainly will too, at a full timeout each.
|
|
200
|
+
if (consecutiveFailures >= 3) {
|
|
201
|
+
warnings.push(`Gave up on ${tier.name} after ${consecutiveFailures} consecutive failures ` +
|
|
202
|
+
`(last: ${result.error ?? `HTTP ${result.status}`}).`);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
warnings.push(`Could not read ${next}: ${result.error ?? `HTTP ${result.status}`}`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
consecutiveFailures = 0;
|
|
209
|
+
let body = result.body;
|
|
210
|
+
if (gzipped) {
|
|
211
|
+
try {
|
|
212
|
+
body = gunzipSync(Buffer.from(result.bytes ?? new Uint8Array())).toString("utf8");
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
warnings.push(`Could not decompress ${next}: ${err.message}`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (isPlainTextSitemap(body)) {
|
|
220
|
+
const plain = extractPlainTextUrls(body).filter(looksAuditable);
|
|
221
|
+
if (plain.length === 0)
|
|
222
|
+
continue;
|
|
223
|
+
sitemapsRead.push(next);
|
|
224
|
+
for (const loc of plain) {
|
|
225
|
+
if (urls.size >= MAX_URLS) {
|
|
226
|
+
truncated = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
urls.add(loc);
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const locs = extractLocs(body);
|
|
234
|
+
if (locs.length === 0)
|
|
235
|
+
continue;
|
|
236
|
+
sitemapsRead.push(next);
|
|
237
|
+
if (isSitemapIndex(body)) {
|
|
238
|
+
for (const child of locs)
|
|
239
|
+
if (!seen.has(child))
|
|
240
|
+
queue.push(child);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
for (const loc of locs) {
|
|
244
|
+
if (urls.size >= MAX_URLS) {
|
|
245
|
+
truncated = true;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
if (looksAuditable(loc))
|
|
249
|
+
urls.add(loc);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (sitemapsRead.length > 0 && urls.size === 0) {
|
|
254
|
+
warnings.push(`Found a sitemap at ${sitemapsRead[0]} but could not extract any URLs from it — ` +
|
|
255
|
+
`its child documents failed to load or contained no page entries. Supply URLs ` +
|
|
256
|
+
`with discovery:"list" or discovery:"csv".`);
|
|
257
|
+
}
|
|
258
|
+
if (sitemapsRead.length === 0) {
|
|
259
|
+
warnings.push(`No sitemap found for ${origin}. Tried ${attempted.length} location(s): ` +
|
|
260
|
+
`robots.txt, the standard paths, the homepage <link rel="sitemap">, and ` +
|
|
261
|
+
`common CMS locations. ` +
|
|
262
|
+
`Supply URLs directly with discovery:"list", or point at a CSV export with ` +
|
|
263
|
+
`discovery:"csv" — an analytics top-pages export is the better input for a ` +
|
|
264
|
+
`performance audit anyway, since it is weighted by real traffic.`);
|
|
265
|
+
}
|
|
266
|
+
if (truncated) {
|
|
267
|
+
warnings.push(`Stopped after ${sitemapsRead.length} sitemap documents and ${urls.size} URLs. ` +
|
|
268
|
+
`The template breakdown below is based on that subset.`);
|
|
269
|
+
}
|
|
270
|
+
return { urls: [...urls], sitemapsRead, truncated, attempted, warnings };
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=sitemapReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemapReader.js","sourceRoot":"","sources":["../../src/utils/sitemapReader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluster a URL list into page templates.
|
|
3
|
+
*
|
|
4
|
+
* A performance audit's unit of analysis is the template, not the URL. Nobody
|
|
5
|
+
* needs 3,904 product pages measured; they need to know what a product page
|
|
6
|
+
* costs. The manual Five Below audit was organised exactly this way — Homepage,
|
|
7
|
+
* PLP (category), PLP (subcategory), PLP (paginated), PDP, Cart, Search,
|
|
8
|
+
* Info — with representative URLs sampled per template, and this reproduces
|
|
9
|
+
* that structure automatically instead of asking someone to hand-write the
|
|
10
|
+
* list.
|
|
11
|
+
*
|
|
12
|
+
* The clustering is a heuristic and will occasionally be wrong. That is
|
|
13
|
+
* acceptable because the plan tool shows its work and the user approves the
|
|
14
|
+
* sample before any PSI call is spent: a misclassification costs a
|
|
15
|
+
* conversation turn, not quota.
|
|
16
|
+
*/
|
|
17
|
+
export interface UrlTemplate {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
/** Path shape with high-cardinality segments collapsed, e.g. "/categories/*". */
|
|
21
|
+
pattern: string;
|
|
22
|
+
urlCount: number;
|
|
23
|
+
/** How many URLs to audit, given the population size. */
|
|
24
|
+
suggestedSample: number;
|
|
25
|
+
/** Representative URLs, most-canonical first. */
|
|
26
|
+
candidates: string[];
|
|
27
|
+
auditable: boolean;
|
|
28
|
+
reason?: string;
|
|
29
|
+
recommendation?: string;
|
|
30
|
+
/**
|
|
31
|
+
* True when *any* URL in the template is session-gated — computed over the
|
|
32
|
+
* whole group, not the sampled candidates. Deciding this from candidates made
|
|
33
|
+
* the verdict depend on which URLs the sampler happened to pick, so a
|
|
34
|
+
* template containing /cart could pass as auditable purely by luck.
|
|
35
|
+
*/
|
|
36
|
+
anySessionGated: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface ClassifyOptions {
|
|
39
|
+
/** Patterns matching fewer URLs than this are folded into "Other". */
|
|
40
|
+
minTemplateSize?: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function classifyUrls(rawUrls: string[], options?: ClassifyOptions): {
|
|
43
|
+
templates: UrlTemplate[];
|
|
44
|
+
skipped: number;
|
|
45
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { isSessionGated } from "./publicUrl.js";
|
|
2
|
+
/**
|
|
3
|
+
* Query parameters that identify the visitor or the campaign rather than the
|
|
4
|
+
* page. Left in, they would split one template into dozens of "shapes" that
|
|
5
|
+
* all render identically.
|
|
6
|
+
*/
|
|
7
|
+
const TRACKING_PARAMS = /^(utm_|gclid|fbclid|msclkid|mc_cid|mc_eid|_ga|igshid|srsltid|ref|referrer|source|cmpid|campaign)/i;
|
|
8
|
+
/**
|
|
9
|
+
* A segment position needs at least this many distinct values to be wildcarded.
|
|
10
|
+
* Two siblings are as likely to be two distinct pages as one template with two
|
|
11
|
+
* instances, so the floor is three.
|
|
12
|
+
*/
|
|
13
|
+
const WILDCARD_DISTINCT = 3;
|
|
14
|
+
/**
|
|
15
|
+
* The signal that separates an identifier from a section name: **fan-out**.
|
|
16
|
+
*
|
|
17
|
+
* Cardinality alone does not work, and getting this wrong is visible on real
|
|
18
|
+
* sites. On nodejs.org, "/en/{blog,download,learn,about}" has only a handful of
|
|
19
|
+
* distinct values, so a pure count threshold wildcarded it and merged 1,054
|
|
20
|
+
* blog posts, download pages and API docs into one meaningless "/en/*\/*\/*"
|
|
21
|
+
* template.
|
|
22
|
+
*
|
|
23
|
+
* A section name is shared by many URLs (blog → 900 pages); an identifier
|
|
24
|
+
* belongs to one (v04.3 → 1 page). So a segment is a wildcard when its values
|
|
25
|
+
* each account for very few URLs, and stays literal when each value heads a
|
|
26
|
+
* substantial subtree — regardless of how many there are.
|
|
27
|
+
*/
|
|
28
|
+
const MAX_MEAN_FANOUT = 2.5;
|
|
29
|
+
/**
|
|
30
|
+
* The first path segment is the site's own taxonomy — "categories", "products",
|
|
31
|
+
* "info" — and collapsing it produces the useless pattern "/*". It only gets
|
|
32
|
+
* wildcarded on a clear id explosion.
|
|
33
|
+
*/
|
|
34
|
+
const WILDCARD_DISTINCT_ROOT = 20;
|
|
35
|
+
const ID_LIKE = /^(\d+|[0-9a-f]{8,}|[0-9a-f-]{16,}|p\d+|sku[-_]?\w+)$/i;
|
|
36
|
+
/**
|
|
37
|
+
* Route vocabulary — segments that name a *kind* of page, never an instance.
|
|
38
|
+
*
|
|
39
|
+
* Fan-out alone misreads these on a small URL list. Given eight URLs under
|
|
40
|
+
* /ecommerce/, the segments {page, product, cart, checkout, my-account} average
|
|
41
|
+
* 1.6 URLs each, which looks exactly like a set of identifiers — so the
|
|
42
|
+
* classifier collapsed paginated listings and product detail pages into one
|
|
43
|
+
* "/ecommerce/*\/*" template and proposed sampling them as if they were the
|
|
44
|
+
* same page type. On the full inventory the fan-out for "product" would be
|
|
45
|
+
* large and the heuristic would work, but a plan should not be wrong just
|
|
46
|
+
* because the input list is short.
|
|
47
|
+
*/
|
|
48
|
+
const ROUTE_WORDS = new Set([
|
|
49
|
+
"page", "pages", "product", "products", "category", "categories", "collection",
|
|
50
|
+
"collections", "cart", "basket", "checkout", "account", "my-account", "login",
|
|
51
|
+
"signin", "register", "search", "tag", "tags", "author", "feed", "blog", "news",
|
|
52
|
+
"article", "articles", "post", "posts", "info", "about", "help", "support",
|
|
53
|
+
"shop", "store", "browse", "dept", "department", "item", "items", "sku",
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* A content slug: hyphenated and long enough to be a title rather than a route.
|
|
57
|
+
* "adrienne-trek-jacket" is an instance; "my-account" is a route.
|
|
58
|
+
*/
|
|
59
|
+
function looksLikeSlug(value) {
|
|
60
|
+
if (!value.includes("-"))
|
|
61
|
+
return false;
|
|
62
|
+
const words = value.split("-").length;
|
|
63
|
+
return value.length >= 14 || words >= 3;
|
|
64
|
+
}
|
|
65
|
+
function parseUrl(raw) {
|
|
66
|
+
let url;
|
|
67
|
+
try {
|
|
68
|
+
url = new URL(raw);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
74
|
+
const keys = [...url.searchParams.keys()]
|
|
75
|
+
.filter((k) => !TRACKING_PARAMS.test(k))
|
|
76
|
+
.map((k) => k.toLowerCase())
|
|
77
|
+
.sort();
|
|
78
|
+
return { raw, segments, querySig: [...new Set(keys)].join("&") };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Wildcard a segment when its values are identifiers rather than taxonomy.
|
|
82
|
+
* Cardinality alone is the main signal; an id-shaped majority is enough on its
|
|
83
|
+
* own, since "/p/12345" and "/p/12346" are the same page template even when
|
|
84
|
+
* only two exist.
|
|
85
|
+
*/
|
|
86
|
+
function shouldWildcard(depth, values) {
|
|
87
|
+
const distinct = new Set(values);
|
|
88
|
+
if (distinct.size < 2)
|
|
89
|
+
return false;
|
|
90
|
+
// Numeric, hash-shaped and long-slug values are identifiers whatever their
|
|
91
|
+
// fan-out; route vocabulary never is.
|
|
92
|
+
const values_ = [...distinct].map((v) => v.toLowerCase());
|
|
93
|
+
const idLike = values_.filter((v) => ID_LIKE.test(v) || looksLikeSlug(v)).length;
|
|
94
|
+
if (idLike / distinct.size > 0.6)
|
|
95
|
+
return true;
|
|
96
|
+
if (values_.every((v) => ROUTE_WORDS.has(v)))
|
|
97
|
+
return false;
|
|
98
|
+
if (depth === 0 && distinct.size < WILDCARD_DISTINCT_ROOT)
|
|
99
|
+
return false;
|
|
100
|
+
if (distinct.size < WILDCARD_DISTINCT)
|
|
101
|
+
return false;
|
|
102
|
+
return values.length / distinct.size <= MAX_MEAN_FANOUT;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Recursive descent over path segments.
|
|
106
|
+
*
|
|
107
|
+
* At each depth, URLs that end here form their own pattern, and the rest are
|
|
108
|
+
* grouped by their next segment — wildcarded together when that segment looks
|
|
109
|
+
* like an identifier, kept apart when it looks like a section name. Recursion
|
|
110
|
+
* (rather than one global pass per depth) is what keeps "/categories/*" and
|
|
111
|
+
* "/products/*" separate instead of collapsing both into "/*\/*".
|
|
112
|
+
*/
|
|
113
|
+
function descend(urls, depth, prefix, out) {
|
|
114
|
+
const terminal = urls.filter((u) => u.segments.length === depth);
|
|
115
|
+
const deeper = urls.filter((u) => u.segments.length > depth);
|
|
116
|
+
if (terminal.length > 0) {
|
|
117
|
+
const key = "/" + prefix.join("/");
|
|
118
|
+
for (const u of terminal) {
|
|
119
|
+
const full = u.querySig ? `${key}?${u.querySig}` : key;
|
|
120
|
+
const bucket = out.get(full);
|
|
121
|
+
if (bucket)
|
|
122
|
+
bucket.push(u);
|
|
123
|
+
else
|
|
124
|
+
out.set(full, [u]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (deeper.length === 0)
|
|
128
|
+
return;
|
|
129
|
+
if (shouldWildcard(depth, deeper.map((u) => u.segments[depth]))) {
|
|
130
|
+
descend(deeper, depth + 1, [...prefix, "*"], out);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const groups = new Map();
|
|
134
|
+
for (const u of deeper) {
|
|
135
|
+
const token = u.segments[depth];
|
|
136
|
+
const bucket = groups.get(token);
|
|
137
|
+
if (bucket)
|
|
138
|
+
bucket.push(u);
|
|
139
|
+
else
|
|
140
|
+
groups.set(token, [u]);
|
|
141
|
+
}
|
|
142
|
+
for (const [token, group] of groups) {
|
|
143
|
+
descend(group, depth + 1, [...prefix, token], out);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Human labels for common commerce and content shapes.
|
|
148
|
+
*
|
|
149
|
+
* Deliberately conservative: an unrecognised pattern is labelled by its path
|
|
150
|
+
* rather than guessed at, because a wrong label in an executive report is
|
|
151
|
+
* worse than a literal one.
|
|
152
|
+
*/
|
|
153
|
+
function labelFor(pattern) {
|
|
154
|
+
const [path, query] = pattern.split("?");
|
|
155
|
+
const p = path.toLowerCase();
|
|
156
|
+
const depth = path.split("/").filter(Boolean).length;
|
|
157
|
+
const wildcards = (path.match(/\*/g) ?? []).length;
|
|
158
|
+
const paginated = query ? ` (${query.includes("p") || query.includes("page") ? "paginated" : "filtered"})` : "";
|
|
159
|
+
if (path === "/")
|
|
160
|
+
return "Homepage";
|
|
161
|
+
if (/(^|\/)(cart|basket|bag)(\/|$)/.test(p))
|
|
162
|
+
return "Cart";
|
|
163
|
+
if (/(^|\/)(checkout|payment)(\/|$)/.test(p))
|
|
164
|
+
return "Checkout";
|
|
165
|
+
if (/(^|\/)(search|s|find)(\/|$)/.test(p))
|
|
166
|
+
return "Search results";
|
|
167
|
+
if (/(^|\/)(store-locator|stores|locations|find-a-store)(\/|$)/.test(p))
|
|
168
|
+
return "Store locator";
|
|
169
|
+
if (/(^|\/)(account|my-account|profile|orders|wishlist)(\/|$)/.test(p))
|
|
170
|
+
return "Account";
|
|
171
|
+
if (/(^|\/)(login|signin|sign-in|register|signup)(\/|$)/.test(p))
|
|
172
|
+
return "Login / register";
|
|
173
|
+
if (/(^|\/)(product|products|p|item|items|sku|dp)(\/|$)/.test(p))
|
|
174
|
+
return `PDP (product)${paginated}`;
|
|
175
|
+
if (/(^|\/)(blog|news|articles|stories|posts)(\/|$)/.test(p))
|
|
176
|
+
return `Content / article${paginated}`;
|
|
177
|
+
if (/(^|\/)(info|about|help|faq|support|legal|privacy|terms|policies|customer-service)(\/|$)/.test(p))
|
|
178
|
+
return `Info / static${paginated}`;
|
|
179
|
+
if (/(^|\/)(categories|category|c|collections|shop|browse|dept|department)(\/|$)/.test(p)) {
|
|
180
|
+
const kind = wildcards >= 2 ? "subcategory" : "category";
|
|
181
|
+
return `PLP (${kind})${paginated}`;
|
|
182
|
+
}
|
|
183
|
+
if (wildcards > 0 && depth <= 2)
|
|
184
|
+
return `Listing${paginated}`;
|
|
185
|
+
return `${path}${paginated}`;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Sample size by population.
|
|
189
|
+
*
|
|
190
|
+
* Three is the ceiling because PSI is slow and quota is finite, and because
|
|
191
|
+
* variance between pages of one template is usually smaller than variance
|
|
192
|
+
* between templates — a fourth PDP tells you much less than a first Cart.
|
|
193
|
+
*/
|
|
194
|
+
function suggestSample(population) {
|
|
195
|
+
if (population <= 1)
|
|
196
|
+
return 1;
|
|
197
|
+
if (population <= 5)
|
|
198
|
+
return 2;
|
|
199
|
+
return 3;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Representatives, chosen for spread rather than at random.
|
|
203
|
+
*
|
|
204
|
+
* Shortest first: it is usually the canonical, most-linked instance of the
|
|
205
|
+
* template. Then the longest and a middle one, because URL length correlates
|
|
206
|
+
* with depth and with how much content the page carries, and a template's
|
|
207
|
+
* worst page is more often its longest.
|
|
208
|
+
*/
|
|
209
|
+
function pickCandidates(urls, count) {
|
|
210
|
+
const sorted = [...urls].sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
211
|
+
if (sorted.length <= count)
|
|
212
|
+
return sorted;
|
|
213
|
+
const picks = [sorted[0]];
|
|
214
|
+
if (count >= 2)
|
|
215
|
+
picks.push(sorted[sorted.length - 1]);
|
|
216
|
+
if (count >= 3)
|
|
217
|
+
picks.push(sorted[Math.floor(sorted.length / 2)]);
|
|
218
|
+
for (let i = 1; picks.length < count && i < sorted.length; i++) {
|
|
219
|
+
if (!picks.includes(sorted[i]))
|
|
220
|
+
picks.push(sorted[i]);
|
|
221
|
+
}
|
|
222
|
+
return picks.slice(0, count);
|
|
223
|
+
}
|
|
224
|
+
function slugify(pattern) {
|
|
225
|
+
return (pattern
|
|
226
|
+
.replace(/[?&=]/g, "-")
|
|
227
|
+
.replace(/\*/g, "wild")
|
|
228
|
+
.replace(/[^a-z0-9]+/gi, "-")
|
|
229
|
+
.replace(/^-+|-+$/g, "")
|
|
230
|
+
.toLowerCase() || "root");
|
|
231
|
+
}
|
|
232
|
+
export function classifyUrls(rawUrls, options = {}) {
|
|
233
|
+
const { minTemplateSize = 1 } = options;
|
|
234
|
+
const parsed = [];
|
|
235
|
+
let skipped = 0;
|
|
236
|
+
for (const raw of rawUrls) {
|
|
237
|
+
const p = parseUrl(raw);
|
|
238
|
+
if (p)
|
|
239
|
+
parsed.push(p);
|
|
240
|
+
else
|
|
241
|
+
skipped++;
|
|
242
|
+
}
|
|
243
|
+
const buckets = new Map();
|
|
244
|
+
descend(parsed, 0, [], buckets);
|
|
245
|
+
const templates = [];
|
|
246
|
+
for (const [pattern, group] of buckets) {
|
|
247
|
+
if (group.length < minTemplateSize)
|
|
248
|
+
continue;
|
|
249
|
+
const urls = group.map((g) => g.raw);
|
|
250
|
+
const suggested = suggestSample(urls.length);
|
|
251
|
+
templates.push({
|
|
252
|
+
id: slugify(pattern),
|
|
253
|
+
label: labelFor(pattern),
|
|
254
|
+
pattern,
|
|
255
|
+
urlCount: urls.length,
|
|
256
|
+
suggestedSample: suggested,
|
|
257
|
+
candidates: pickCandidates(urls, suggested),
|
|
258
|
+
auditable: true,
|
|
259
|
+
anySessionGated: urls.some(isSessionGated),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// Biggest templates first: a reader scanning the plan should see the shapes
|
|
263
|
+
// that dominate the site before the long tail of one-off pages.
|
|
264
|
+
templates.sort((a, b) => b.urlCount - a.urlCount || a.pattern.localeCompare(b.pattern));
|
|
265
|
+
return { templates, skipped };
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=urlClassifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"urlClassifier.js","sourceRoot":"","sources":["../../src/utils/urlClassifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One input, three shapes.
|
|
3
|
+
*
|
|
4
|
+
* People arrive with a URL, a list they pasted from a spreadsheet, or a CSV
|
|
5
|
+
* export. Making them say which is which is a tax on the common case, and an
|
|
6
|
+
* agent relaying a user's request should not have to guess a parameter name.
|
|
7
|
+
* So the `url` input accepts any of the three and this module works out what it
|
|
8
|
+
* was given.
|
|
9
|
+
*
|
|
10
|
+
* Detection is by shape, not by a flag, and deliberately conservative: anything
|
|
11
|
+
* ambiguous is reported rather than guessed at, because silently auditing the
|
|
12
|
+
* wrong set of pages is worse than an error.
|
|
13
|
+
*/
|
|
14
|
+
export type UrlInputKind = "single" | "list" | "csv";
|
|
15
|
+
export interface ResolvedUrls {
|
|
16
|
+
urls: string[];
|
|
17
|
+
kind: UrlInputKind;
|
|
18
|
+
/** Where a CSV was read from, or the column used. Empty for the other kinds. */
|
|
19
|
+
source?: string;
|
|
20
|
+
warnings: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve whatever was handed in into a URL list.
|
|
24
|
+
*
|
|
25
|
+
* Order matters: the CSV check runs first because a path can contain no commas
|
|
26
|
+
* and no scheme, which would otherwise be coerced into `https://./file.csv`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveUrlInput(input: string): ResolvedUrls;
|
|
29
|
+
/** Accept an explicit array too, for callers that already have one. */
|
|
30
|
+
export declare function resolveUrlInputs(input: string | undefined, urls: string[] | undefined): ResolvedUrls;
|