@anubhav-choubey/web-for-agents 0.1.0 → 0.3.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 +77 -512
- package/dist/bin.js +6 -6
- package/dist/{chunk-AZG5MLNG.js → chunk-2Y3LLYEC.js} +5 -1
- package/dist/{chunk-IF32TPGR.js → chunk-357ATBMS.js} +369 -130
- package/dist/chunk-4STTPCBR.js +25 -0
- package/dist/chunk-CGIIB6AF.js +127 -0
- package/dist/{chunk-HONWT6GK.js → chunk-K3B53RIO.js} +66 -34
- package/dist/chunk-L3AYWCSX.js +33 -0
- package/dist/chunk-MIVGE5TM.js +34 -0
- package/dist/chunk-OKCYZE6C.js +245 -0
- package/dist/{chunk-5TNG4QMP.js → chunk-PGZH33B5.js} +20 -2
- package/dist/{chunk-ZYMNO5AO.js → chunk-T42S3WRV.js} +161 -62
- package/dist/cli.js +6 -6
- package/dist/{happyDom-XVDHYJDM.js → happyDom-ZEFNMD7B.js} +3 -3
- package/dist/index.d.ts +89 -11
- package/dist/index.js +113 -246
- package/dist/overlay.js +22 -42
- package/dist/{renderChild-Y2RFSYY4.js → renderChild-PSQLAIUW.js} +27 -4
- package/dist/vite.d.ts +116 -3
- package/dist/vite.js +867 -192
- package/package.json +1 -1
- package/dist/chunk-CKXHTGGT.js +0 -25
- package/dist/chunk-FEAKVNMG.js +0 -12
- package/dist/chunk-LCLDQ3A3.js +0 -26
- package/dist/chunk-LCVI2PRP.js +0 -13
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AI_BOTS
|
|
3
|
-
|
|
2
|
+
AI_BOTS,
|
|
3
|
+
scanHtml
|
|
4
|
+
} from "./chunk-CGIIB6AF.js";
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
DESCRIPTION_MAX_CHARS,
|
|
7
|
+
DESCRIPTION_MIN_CHARS,
|
|
8
|
+
TITLE_MAX_CHARS
|
|
9
|
+
} from "./chunk-L3AYWCSX.js";
|
|
7
10
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
MAX_JSON_LD_NODES,
|
|
12
|
+
jsonLdNodes,
|
|
13
|
+
jsonLdTypes,
|
|
14
|
+
nodeTypes
|
|
15
|
+
} from "./chunk-MIVGE5TM.js";
|
|
10
16
|
import {
|
|
17
|
+
AGENT_MAP_VERSION,
|
|
11
18
|
collapseWhitespace,
|
|
12
19
|
stripBom
|
|
13
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-4STTPCBR.js";
|
|
14
21
|
import {
|
|
15
22
|
AGENT_MAP_PATH,
|
|
16
23
|
LLMS_TXT_PATH,
|
|
@@ -22,12 +29,177 @@ import {
|
|
|
22
29
|
markdownPathFor,
|
|
23
30
|
trimTrailingSlash,
|
|
24
31
|
tryResolveUrl
|
|
25
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-2Y3LLYEC.js";
|
|
26
33
|
import {
|
|
27
34
|
errorMessage,
|
|
28
35
|
withPrefix
|
|
29
36
|
} from "./chunk-JJN6FNJD.js";
|
|
30
37
|
|
|
38
|
+
// src/shared/schemaRules.ts
|
|
39
|
+
var DOCS = "https://developers.google.com/search/docs/appearance/structured-data/";
|
|
40
|
+
var req = (...anyOf) => ({ anyOf });
|
|
41
|
+
var ARTICLE = {
|
|
42
|
+
docs: `${DOCS}article`,
|
|
43
|
+
required: [],
|
|
44
|
+
recommended: ["author", "author.name", "author.url", "dateModified", "datePublished", "headline", "image"]
|
|
45
|
+
};
|
|
46
|
+
var SOFTWARE_APPLICATION = {
|
|
47
|
+
docs: `${DOCS}software-app`,
|
|
48
|
+
required: [req("name"), req("offers"), req("aggregateRating", "review")],
|
|
49
|
+
recommended: ["applicationCategory", "operatingSystem"],
|
|
50
|
+
nested: [{ prop: "offers", whenType: ["Offer"], orUntyped: true, required: [req("price")] }]
|
|
51
|
+
};
|
|
52
|
+
var SCHEMA_RULES = {
|
|
53
|
+
Article: ARTICLE,
|
|
54
|
+
NewsArticle: ARTICLE,
|
|
55
|
+
BlogPosting: ARTICLE,
|
|
56
|
+
BreadcrumbList: {
|
|
57
|
+
docs: `${DOCS}breadcrumb`,
|
|
58
|
+
required: [req("itemListElement")],
|
|
59
|
+
recommended: [],
|
|
60
|
+
nested: [
|
|
61
|
+
{
|
|
62
|
+
prop: "itemListElement",
|
|
63
|
+
required: [req("position"), req("name", "item.name"), { anyOf: ["item"], exceptLast: true }]
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
Event: {
|
|
68
|
+
docs: `${DOCS}event`,
|
|
69
|
+
required: [req("name"), req("startDate"), req("location")],
|
|
70
|
+
recommended: ["description", "endDate", "eventStatus", "image", "location.name", "offers", "organizer", "performer"],
|
|
71
|
+
nested: [{ prop: "location", whenType: ["Place"], required: [req("address")] }]
|
|
72
|
+
},
|
|
73
|
+
FAQPage: {
|
|
74
|
+
docs: `${DOCS}faqpage`,
|
|
75
|
+
required: [req("mainEntity")],
|
|
76
|
+
recommended: [],
|
|
77
|
+
nested: [
|
|
78
|
+
{
|
|
79
|
+
prop: "mainEntity",
|
|
80
|
+
required: [req("name"), req("acceptedAnswer")],
|
|
81
|
+
nested: [{ prop: "acceptedAnswer", required: [req("text")] }]
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
note: "Google no longer shows FAQ rich results; answer engines may still use it",
|
|
85
|
+
retired: true
|
|
86
|
+
},
|
|
87
|
+
Organization: {
|
|
88
|
+
docs: `${DOCS}organization`,
|
|
89
|
+
required: [],
|
|
90
|
+
recommended: ["name", "url", "logo", "description", "sameAs", "address", "contactPoint", "email", "telephone"]
|
|
91
|
+
},
|
|
92
|
+
Person: {
|
|
93
|
+
docs: `${DOCS}profile-page`,
|
|
94
|
+
required: [],
|
|
95
|
+
recommended: ["name", "alternateName", "description", "identifier", "image", "sameAs"]
|
|
96
|
+
},
|
|
97
|
+
ProfilePage: {
|
|
98
|
+
docs: `${DOCS}profile-page`,
|
|
99
|
+
required: [req("mainEntity")],
|
|
100
|
+
recommended: ["dateCreated", "dateModified"],
|
|
101
|
+
nested: [{ prop: "mainEntity", required: [req("name", "alternateName")] }]
|
|
102
|
+
},
|
|
103
|
+
Product: {
|
|
104
|
+
docs: `${DOCS}product-snippet`,
|
|
105
|
+
required: [req("name"), req("offers", "aggregateRating", "review")],
|
|
106
|
+
recommended: ["aggregateRating", "offers", "review", "offers.priceCurrency", "offers.availability"],
|
|
107
|
+
nested: [
|
|
108
|
+
{ prop: "offers", whenType: ["Offer"], orUntyped: true, required: [req("price", "priceSpecification.price")] },
|
|
109
|
+
{ prop: "offers", whenType: ["AggregateOffer"], required: [req("lowPrice"), req("priceCurrency")] }
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
Recipe: {
|
|
113
|
+
docs: `${DOCS}recipe`,
|
|
114
|
+
required: [req("name"), req("image")],
|
|
115
|
+
recommended: ["author", "datePublished", "description", "recipeIngredient", "recipeInstructions", "totalTime"]
|
|
116
|
+
},
|
|
117
|
+
SoftwareApplication: SOFTWARE_APPLICATION,
|
|
118
|
+
MobileApplication: SOFTWARE_APPLICATION,
|
|
119
|
+
WebApplication: SOFTWARE_APPLICATION,
|
|
120
|
+
WebSite: {
|
|
121
|
+
docs: "https://developers.google.com/search/docs/appearance/site-names",
|
|
122
|
+
required: [req("name"), req("url")],
|
|
123
|
+
recommended: ["alternateName"]
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
function present(v) {
|
|
127
|
+
if (v === void 0 || v === null) return false;
|
|
128
|
+
if (typeof v === "string") return v.trim() !== "";
|
|
129
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
var isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
133
|
+
function hasPath(node, path2) {
|
|
134
|
+
let level = [node];
|
|
135
|
+
for (const key of path2.split(".")) {
|
|
136
|
+
const next = [];
|
|
137
|
+
for (const v of level) {
|
|
138
|
+
for (const o of Array.isArray(v) ? v : [v]) if (isObject(o) && present(o[key])) next.push(o[key]);
|
|
139
|
+
}
|
|
140
|
+
if (next.length === 0) return false;
|
|
141
|
+
level = next;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
function resolveRef(o, byId) {
|
|
146
|
+
const keys = Object.keys(o);
|
|
147
|
+
if (keys.length !== 1 || keys[0] !== "@id") return o;
|
|
148
|
+
return typeof o["@id"] === "string" ? byId.get(o["@id"]) ?? null : null;
|
|
149
|
+
}
|
|
150
|
+
var label = (prefix, anyOf) => prefix + anyOf[0] + (anyOf.length > 1 ? ` (or ${anyOf.slice(1).join(", ")})` : "");
|
|
151
|
+
function checkSchemaNodes(nodes, limit = Infinity) {
|
|
152
|
+
const byId = /* @__PURE__ */ new Map();
|
|
153
|
+
for (const n of nodes) if (typeof n["@id"] === "string" && !byId.has(n["@id"])) byId.set(n["@id"], n);
|
|
154
|
+
const scoredIssues = [];
|
|
155
|
+
const retiredIssues = [];
|
|
156
|
+
let total = 0;
|
|
157
|
+
let scored = 0;
|
|
158
|
+
let budget = MAX_JSON_LD_NODES;
|
|
159
|
+
const report = (type, path2) => {
|
|
160
|
+
total++;
|
|
161
|
+
const retired = SCHEMA_RULES[type].retired === true;
|
|
162
|
+
if (!retired) scored++;
|
|
163
|
+
const list = retired ? retiredIssues : scoredIssues;
|
|
164
|
+
if (list.length < limit) list.push({ type, path: path2 });
|
|
165
|
+
};
|
|
166
|
+
const apply = (type, node, prefix, required, nested, isLast) => {
|
|
167
|
+
for (const r of required) {
|
|
168
|
+
if (r.exceptLast && isLast) continue;
|
|
169
|
+
if (!r.anyOf.some((p) => hasPath(node, p))) report(type, label(prefix, r.anyOf));
|
|
170
|
+
}
|
|
171
|
+
for (const n of nested ?? []) {
|
|
172
|
+
const v = node[n.prop];
|
|
173
|
+
const items = Array.isArray(v) ? v : [v];
|
|
174
|
+
for (let i = 0; i < items.length; i++) {
|
|
175
|
+
if (--budget < 0) return;
|
|
176
|
+
const raw = items[i];
|
|
177
|
+
if (!isObject(raw)) continue;
|
|
178
|
+
const child = resolveRef(raw, byId);
|
|
179
|
+
if (child === null) continue;
|
|
180
|
+
if (n.whenType !== void 0) {
|
|
181
|
+
const types = nodeTypes(child);
|
|
182
|
+
const matches2 = types.length === 0 ? n.orUntyped === true : types.some((t) => n.whenType.includes(t));
|
|
183
|
+
if (!matches2) continue;
|
|
184
|
+
}
|
|
185
|
+
const at = `${prefix}${n.prop}${Array.isArray(v) ? `[${i}]` : ""}.`;
|
|
186
|
+
apply(type, child, at, n.required, n.nested, i === items.length - 1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
for (const node of nodes) {
|
|
191
|
+
if (--budget < 0) break;
|
|
192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
193
|
+
for (const type of nodeTypes(node)) {
|
|
194
|
+
const rule = Object.hasOwn(SCHEMA_RULES, type) ? SCHEMA_RULES[type] : void 0;
|
|
195
|
+
if (rule === void 0 || seen.has(rule)) continue;
|
|
196
|
+
seen.add(rule);
|
|
197
|
+
apply(type, node, "", rule.required, rule.nested, true);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { issues: [...scoredIssues, ...retiredIssues].slice(0, limit), total, scored };
|
|
201
|
+
}
|
|
202
|
+
|
|
31
203
|
// src/cli/privateAddress.ts
|
|
32
204
|
import { lookup } from "dns/promises";
|
|
33
205
|
import { BlockList, isIP } from "net";
|
|
@@ -79,10 +251,10 @@ function abortable(p, signal) {
|
|
|
79
251
|
});
|
|
80
252
|
}
|
|
81
253
|
function decode(bytes, contentType) {
|
|
82
|
-
const
|
|
254
|
+
const label2 = /charset\s*=\s*"?([^;"\s]+)/i.exec(contentType)?.[1] ?? "utf-8";
|
|
83
255
|
let decoder;
|
|
84
256
|
try {
|
|
85
|
-
decoder = new TextDecoder(
|
|
257
|
+
decoder = new TextDecoder(label2);
|
|
86
258
|
} catch (e) {
|
|
87
259
|
if (!(e instanceof RangeError)) throw e;
|
|
88
260
|
decoder = new TextDecoder("utf-8");
|
|
@@ -266,16 +438,6 @@ function decode2(s) {
|
|
|
266
438
|
return String.fromCodePoint(cp);
|
|
267
439
|
});
|
|
268
440
|
}
|
|
269
|
-
var isSpace = (c) => c === 32 || c === 9 || c === 10 || c === 12 || c === 13;
|
|
270
|
-
var isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
271
|
-
function findRawEnd(html, name, from) {
|
|
272
|
-
let i = html.indexOf("</", from);
|
|
273
|
-
while (i !== -1) {
|
|
274
|
-
if (html.slice(i + 2, i + 2 + name.length).toLowerCase() === name) return i;
|
|
275
|
-
i = html.indexOf("</", i + 2);
|
|
276
|
-
}
|
|
277
|
-
return -1;
|
|
278
|
-
}
|
|
279
441
|
function extractHtmlFacts(html) {
|
|
280
442
|
const metas = [];
|
|
281
443
|
const links = [];
|
|
@@ -286,88 +448,29 @@ function extractHtmlFacts(html) {
|
|
|
286
448
|
let prerendered = false;
|
|
287
449
|
const depth = {};
|
|
288
450
|
let excluded = 0;
|
|
289
|
-
|
|
290
|
-
let
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const end = html.indexOf(">", lt + 2);
|
|
305
|
-
i = end === -1 ? n : end + 1;
|
|
451
|
+
let pos = 0;
|
|
452
|
+
let lastAttrs = {};
|
|
453
|
+
for (const t of scanHtml(html, RAW_TEXT)) {
|
|
454
|
+
if (t.start > pos && excluded === 0) chunks.push(decode2(html.slice(pos, t.start)));
|
|
455
|
+
pos = t.end;
|
|
456
|
+
if (t.type === "comment") continue;
|
|
457
|
+
if (t.type === "raw") {
|
|
458
|
+
const raw = html.slice(t.start, t.end);
|
|
459
|
+
if (t.name === "title") {
|
|
460
|
+
if (title === null && (depth.svg ?? 0) === 0) {
|
|
461
|
+
title = decode2(raw).replace(/\s+/g, " ").trim();
|
|
462
|
+
}
|
|
463
|
+
} else if (t.name === "script") {
|
|
464
|
+
const type = (lastAttrs.type ?? "").split(";")[0].trim().toLowerCase();
|
|
465
|
+
if (type === "application/ld+json") jsonLd.push(raw);
|
|
306
466
|
}
|
|
307
467
|
continue;
|
|
308
468
|
}
|
|
309
|
-
const closing
|
|
310
|
-
const nameStart = lt + (closing ? 2 : 1);
|
|
311
|
-
if (!isAlpha(nameStart < n ? html.charCodeAt(nameStart) : 0)) {
|
|
312
|
-
if (excluded === 0) chunks.push("<");
|
|
313
|
-
i = lt + 1;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
let p = nameStart;
|
|
317
|
-
while (p < n) {
|
|
318
|
-
const c = html.charCodeAt(p);
|
|
319
|
-
if (isSpace(c) || c === 62 || c === 47) break;
|
|
320
|
-
p++;
|
|
321
|
-
}
|
|
322
|
-
const name = html.slice(nameStart, p).toLowerCase();
|
|
469
|
+
const { name, closing, selfClosing } = t;
|
|
323
470
|
const attrs = {};
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const c = html.charCodeAt(p);
|
|
327
|
-
if (isSpace(c)) {
|
|
328
|
-
p++;
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
if (c === 62) {
|
|
332
|
-
p++;
|
|
333
|
-
break;
|
|
334
|
-
}
|
|
335
|
-
if (c === 47) {
|
|
336
|
-
selfClosing = p + 1 < n && html.charCodeAt(p + 1) === 62;
|
|
337
|
-
p++;
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
const as = p;
|
|
341
|
-
while (p < n) {
|
|
342
|
-
const d = html.charCodeAt(p);
|
|
343
|
-
if (isSpace(d) || d === 62 || d === 47 || d === 61) break;
|
|
344
|
-
p++;
|
|
345
|
-
}
|
|
346
|
-
if (p === as) {
|
|
347
|
-
p++;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
const aname = html.slice(as, p).toLowerCase();
|
|
351
|
-
while (p < n && isSpace(html.charCodeAt(p))) p++;
|
|
352
|
-
let value = "";
|
|
353
|
-
if (p < n && html.charCodeAt(p) === 61) {
|
|
354
|
-
p++;
|
|
355
|
-
while (p < n && isSpace(html.charCodeAt(p))) p++;
|
|
356
|
-
const q = p < n ? html[p] : "";
|
|
357
|
-
if (q === '"' || q === "'") {
|
|
358
|
-
const end = html.indexOf(q, p + 1);
|
|
359
|
-
const stop = end === -1 ? n : end;
|
|
360
|
-
value = html.slice(p + 1, stop);
|
|
361
|
-
p = end === -1 ? n : end + 1;
|
|
362
|
-
} else {
|
|
363
|
-
const vs = p;
|
|
364
|
-
while (p < n && !isSpace(html.charCodeAt(p)) && html.charCodeAt(p) !== 62) p++;
|
|
365
|
-
value = html.slice(vs, p);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (!(aname in attrs) && !closing) attrs[aname] = decode2(value);
|
|
471
|
+
if (!closing) {
|
|
472
|
+
for (const a of t.attrs) if (!(a.name in attrs)) attrs[a.name] = decode2(a.value);
|
|
369
473
|
}
|
|
370
|
-
i = p;
|
|
371
474
|
if (closing) {
|
|
372
475
|
if (EXCLUDED.has(name) && (depth[name] ?? 0) > 0) {
|
|
373
476
|
depth[name]--;
|
|
@@ -376,6 +479,7 @@ function extractHtmlFacts(html) {
|
|
|
376
479
|
if (excluded === 0 && !INLINE.has(name)) chunks.push(" ");
|
|
377
480
|
continue;
|
|
378
481
|
}
|
|
482
|
+
lastAttrs = attrs;
|
|
379
483
|
if ((depth.head ?? 0) > 0 && excluded === depth.head && !HEAD_OK.has(name)) {
|
|
380
484
|
excluded -= depth.head;
|
|
381
485
|
depth.head = 0;
|
|
@@ -387,27 +491,14 @@ function extractHtmlFacts(html) {
|
|
|
387
491
|
} else if (name === "link") {
|
|
388
492
|
links.push(attrs);
|
|
389
493
|
}
|
|
390
|
-
if (RAW_TEXT.has(name) && !selfClosing)
|
|
391
|
-
const end = findRawEnd(html, name, i);
|
|
392
|
-
const raw = html.slice(i, end === -1 ? n : end);
|
|
393
|
-
i = end === -1 ? n : end;
|
|
394
|
-
if (name === "title") {
|
|
395
|
-
if (title === null && (depth.svg ?? 0) === 0) {
|
|
396
|
-
title = decode2(raw).replace(/\s+/g, " ").trim();
|
|
397
|
-
}
|
|
398
|
-
} else if (name === "script") {
|
|
399
|
-
const type = (attrs.type ?? "").split(";")[0].trim().toLowerCase();
|
|
400
|
-
if (type === "application/ld+json") jsonLd.push(raw);
|
|
401
|
-
}
|
|
402
|
-
if (EXCLUDED.has(name) && end === -1) break;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
494
|
+
if (RAW_TEXT.has(name) && !selfClosing) continue;
|
|
405
495
|
if (EXCLUDED.has(name) && !selfClosing) {
|
|
406
496
|
depth[name] = (depth[name] ?? 0) + 1;
|
|
407
497
|
excluded++;
|
|
408
498
|
}
|
|
409
499
|
if (excluded === 0 && !INLINE.has(name)) chunks.push(" ");
|
|
410
500
|
}
|
|
501
|
+
if (pos < html.length && excluded === 0) chunks.push(decode2(html.slice(pos)));
|
|
411
502
|
return {
|
|
412
503
|
title,
|
|
413
504
|
metas,
|
|
@@ -515,13 +606,13 @@ function matches(pattern, text) {
|
|
|
515
606
|
return last === "" || text.indexOf(last, at) >= 0;
|
|
516
607
|
}
|
|
517
608
|
var matchCache = /* @__PURE__ */ new WeakMap();
|
|
518
|
-
function cachedMatch(robots, pattern,
|
|
609
|
+
function cachedMatch(robots, pattern, path2) {
|
|
519
610
|
let byPath = matchCache.get(robots);
|
|
520
611
|
if (!byPath) matchCache.set(robots, byPath = /* @__PURE__ */ new Map());
|
|
521
|
-
let byPattern = byPath.get(
|
|
522
|
-
if (!byPattern) byPath.set(
|
|
612
|
+
let byPattern = byPath.get(path2);
|
|
613
|
+
if (!byPattern) byPath.set(path2, byPattern = /* @__PURE__ */ new Map());
|
|
523
614
|
let hit = byPattern.get(pattern);
|
|
524
|
-
if (hit === void 0) byPattern.set(pattern, hit = matches(pattern,
|
|
615
|
+
if (hit === void 0) byPattern.set(pattern, hit = matches(pattern, path2));
|
|
525
616
|
return hit;
|
|
526
617
|
}
|
|
527
618
|
function isAllowed(robots, userAgent, pathAndQuery2) {
|
|
@@ -556,9 +647,7 @@ var CHECK_WEIGHTS = {
|
|
|
556
647
|
};
|
|
557
648
|
var CONTENT_MIN_CHARS = 200;
|
|
558
649
|
var CONTENT_WARN_CHARS = 50;
|
|
559
|
-
var
|
|
560
|
-
var DESCRIPTION_MIN_CHARS = 50;
|
|
561
|
-
var DESCRIPTION_MAX_CHARS = 160;
|
|
650
|
+
var SCHEMA_ISSUES_SHOWN = 3;
|
|
562
651
|
var MAX_BODY_LABEL = `${MAX_BODY_BYTES / (1024 * 1024)} MiB`;
|
|
563
652
|
var TITLES = {
|
|
564
653
|
content: "Content without JavaScript",
|
|
@@ -681,7 +770,32 @@ function checkJsonLd({ html }) {
|
|
|
681
770
|
if (!parsed.some((p) => jsonLdTypes(p).length > 0)) {
|
|
682
771
|
return warn("JSON-LD parses but has no @type", "Add an @type (for example Organization) to your JSON-LD.");
|
|
683
772
|
}
|
|
684
|
-
|
|
773
|
+
const { issues, total, scored } = checkSchemaNodes(parsed.flatMap(jsonLdNodes), SCHEMA_ISSUES_SHOWN);
|
|
774
|
+
const base = `${parsed.length} JSON-LD block${parsed.length === 1 ? " parses" : "s parse"} with an @type`;
|
|
775
|
+
if (total === 0) return pass(base);
|
|
776
|
+
const isRetired = (i) => SCHEMA_RULES[i.type].retired === true;
|
|
777
|
+
const shownScored = issues.filter((i) => !isRetired(i));
|
|
778
|
+
const shownRetired = issues.filter(isRetired);
|
|
779
|
+
const retiredNote = notScored(shownRetired, total - scored - shownRetired.length);
|
|
780
|
+
if (scored === 0) return pass(`${base}; ${retiredNote}`);
|
|
781
|
+
const moreScored = scored - shownScored.length;
|
|
782
|
+
const rules = [...new Set(shownScored.map((i) => SCHEMA_RULES[i.type]))];
|
|
783
|
+
return warn(
|
|
784
|
+
`Missing required structured data fields: ${shownScored.map((i) => `${i.type} ${i.path}`).join("; ")}` + (moreScored > 0 ? `; +${moreScored} more` : "") + (total > scored ? `. ${retiredNote}` : ""),
|
|
785
|
+
`Add the missing properties; Google requires them for rich results (${rules.map((r) => r.docs).join(", ")}).`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
var RETIRED_RULES = [...new Set(Object.values(SCHEMA_RULES))].filter((r) => r.retired === true);
|
|
789
|
+
var retiredNotes = (rules) => rules.flatMap((r) => r.note !== void 0 ? [r.note] : []).join("; ");
|
|
790
|
+
function notScored(shown, more) {
|
|
791
|
+
if (shown.length === 0) {
|
|
792
|
+
return `+${more} more not scored (${retiredNotes(RETIRED_RULES)})`;
|
|
793
|
+
}
|
|
794
|
+
const byType = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const i of shown) byType.set(i.type, [...byType.get(i.type) ?? [], i.path]);
|
|
796
|
+
const listed = [...byType].map(([type, paths]) => `${type}: ${paths.join(", ")}`).join("; ");
|
|
797
|
+
const notes = retiredNotes([...new Set(shown.map((i) => SCHEMA_RULES[i.type]))]);
|
|
798
|
+
return `${listed}${more > 0 ? `, +${more} more` : ""} missing${notes ? ` (${notes})` : ""}`;
|
|
685
799
|
}
|
|
686
800
|
function checkRobots({ page, robots }) {
|
|
687
801
|
const missingFix = `Add ${ROBOTS_PATH}; webForAgents() generates one from robots.ai.training / robots.ai.search.`;
|
|
@@ -695,21 +809,21 @@ function checkRobots({ page, robots }) {
|
|
|
695
809
|
const out = robotsVerdict(parsed, pathAndQuery(page.finalUrl));
|
|
696
810
|
return parsed.truncated ? { ...out, message: `${out.message} (${parsed.truncated})` } : out;
|
|
697
811
|
}
|
|
698
|
-
function robotsVerdict(parsed,
|
|
699
|
-
if (!isAllowed(parsed, "*",
|
|
812
|
+
function robotsVerdict(parsed, path2) {
|
|
813
|
+
if (!isAllowed(parsed, "*", path2)) {
|
|
700
814
|
return fail(
|
|
701
|
-
`${ROBOTS_PATH} blocks ${
|
|
815
|
+
`${ROBOTS_PATH} blocks ${path2} for all crawlers`,
|
|
702
816
|
"Remove the Disallow rule matching this path under User-agent: *."
|
|
703
817
|
);
|
|
704
818
|
}
|
|
705
|
-
const searchBlocked = AI_BOTS.search.filter((b) => !isAllowed(parsed, b,
|
|
819
|
+
const searchBlocked = AI_BOTS.search.filter((b) => !isAllowed(parsed, b, path2));
|
|
706
820
|
if (searchBlocked.length > 0) {
|
|
707
821
|
return warn(
|
|
708
822
|
`AI search bots blocked: ${searchBlocked.join(", ")}`,
|
|
709
823
|
'Allow AI search bots (robots.ai.search: "allow") so answer engines can cite this page.'
|
|
710
824
|
);
|
|
711
825
|
}
|
|
712
|
-
const trainingBlocked = AI_BOTS.training.filter((b) => !isAllowed(parsed, b,
|
|
826
|
+
const trainingBlocked = AI_BOTS.training.filter((b) => !isAllowed(parsed, b, path2));
|
|
713
827
|
if (trainingBlocked.length > 0) {
|
|
714
828
|
return pass(`Training crawlers blocked (${trainingBlocked.join(", ")}); search bots allowed`);
|
|
715
829
|
}
|
|
@@ -875,7 +989,7 @@ async function auditWithHooks(url, opts = {}, hooks = {}) {
|
|
|
875
989
|
return report;
|
|
876
990
|
}
|
|
877
991
|
const html = extractHtmlFacts(page.body);
|
|
878
|
-
const at = (
|
|
992
|
+
const at = (path2) => new URL(path2, page.finalUrl).href;
|
|
879
993
|
const getSameOrigin = (u, accept) => sameOrigin(u, page.finalUrl) ? get(u, accept) : Promise.resolve(notFetched(u));
|
|
880
994
|
const alt = markdownAlternate(html);
|
|
881
995
|
const altUrl = alt === null ? null : tryResolveUrl(alt, page.finalUrl);
|
|
@@ -945,8 +1059,9 @@ function normalizeInputUrl(input) {
|
|
|
945
1059
|
return u.href;
|
|
946
1060
|
}
|
|
947
1061
|
var USAGE = `Usage: web-for-agents audit <url> [options]
|
|
1062
|
+
web-for-agents generate <outDir> --site-url <url> [options]
|
|
948
1063
|
|
|
949
|
-
|
|
1064
|
+
audit: check how readable a page is for AI agents and crawlers.
|
|
950
1065
|
|
|
951
1066
|
Options:
|
|
952
1067
|
--json print the report as JSON (page data is not sanitized; control
|
|
@@ -959,6 +1074,22 @@ Options:
|
|
|
959
1074
|
link-local address (default: follow them and print a warning)
|
|
960
1075
|
-h, --help show this help
|
|
961
1076
|
|
|
1077
|
+
generate: write llms.txt, llms-full.txt, .md pages, robots.txt, sitemap.xml and
|
|
1078
|
+
agents.json for HTML you prerendered yourself.
|
|
1079
|
+
|
|
1080
|
+
Options:
|
|
1081
|
+
--site-url <url> absolute URL of the deployed site (or siteUrl in --config)
|
|
1082
|
+
--ai-training <allow|block>
|
|
1083
|
+
--ai-search <allow|block>
|
|
1084
|
+
robots.txt policy for AI crawlers; both are required here or
|
|
1085
|
+
as robots.ai in --config
|
|
1086
|
+
--config <file> .json, .js or .mjs file whose default export holds options
|
|
1087
|
+
(exclude, markdown, llms, agentMap, sitemap, robots, ...)
|
|
1088
|
+
--no-link-markdown do not add the markdown <link> to each page's <head>
|
|
1089
|
+
--overwrite replace an existing robots.txt, sitemap.xml, llms.txt,
|
|
1090
|
+
llms-full.txt or agents.json that differs
|
|
1091
|
+
--json print the result as JSON
|
|
1092
|
+
|
|
962
1093
|
Exit codes: 0 ok, 1 score below --min-score, 2 usage error or fatal error
|
|
963
1094
|
`;
|
|
964
1095
|
function parseCliArgs(argv) {
|
|
@@ -973,6 +1104,12 @@ function parseCliArgs(argv) {
|
|
|
973
1104
|
timeout: { type: "string" },
|
|
974
1105
|
"agent-map": { type: "string" },
|
|
975
1106
|
"no-private-redirects": { type: "boolean" },
|
|
1107
|
+
"site-url": { type: "string" },
|
|
1108
|
+
"ai-training": { type: "string" },
|
|
1109
|
+
"ai-search": { type: "string" },
|
|
1110
|
+
config: { type: "string" },
|
|
1111
|
+
"no-link-markdown": { type: "boolean" },
|
|
1112
|
+
overwrite: { type: "boolean" },
|
|
976
1113
|
help: { type: "boolean", short: "h" }
|
|
977
1114
|
}
|
|
978
1115
|
});
|
|
@@ -982,7 +1119,34 @@ function parseCliArgs(argv) {
|
|
|
982
1119
|
const { values, positionals } = parsed;
|
|
983
1120
|
if (values.help || positionals.length === 0) return { command: "help", json: false, timeoutMs: DEFAULT_TIMEOUT_MS };
|
|
984
1121
|
const [command, input, ...rest] = positionals;
|
|
1122
|
+
const GENERATE_FLAGS = ["site-url", "ai-training", "ai-search", "config", "no-link-markdown", "overwrite"];
|
|
1123
|
+
const AUDIT_FLAGS = ["min-score", "timeout", "agent-map", "no-private-redirects"];
|
|
1124
|
+
if (command === "generate") {
|
|
1125
|
+
if (input === void 0) return fail2("generate requires an output directory");
|
|
1126
|
+
if (rest.length > 0) return fail2(`unexpected argument "${rest[0]}"`);
|
|
1127
|
+
const wrong2 = AUDIT_FLAGS.find((f) => values[f] !== void 0);
|
|
1128
|
+
if (wrong2) return fail2(`--${wrong2} is not an option of generate`);
|
|
1129
|
+
const gen = {
|
|
1130
|
+
outDir: input,
|
|
1131
|
+
linkMarkdown: values["no-link-markdown"] !== true,
|
|
1132
|
+
overwrite: values.overwrite === true
|
|
1133
|
+
};
|
|
1134
|
+
for (const [flag, key] of [["ai-training", "aiTraining"], ["ai-search", "aiSearch"]]) {
|
|
1135
|
+
const v = values[flag];
|
|
1136
|
+
if (v === void 0) continue;
|
|
1137
|
+
if (v !== "allow" && v !== "block") return fail2(`--${flag} must be "allow" or "block", got "${v}"`);
|
|
1138
|
+
gen[key] = v;
|
|
1139
|
+
}
|
|
1140
|
+
if (values["site-url"] !== void 0) gen.siteUrl = values["site-url"];
|
|
1141
|
+
if (values.config !== void 0) {
|
|
1142
|
+
if (values.config.trim() === "") return fail2("--config must be a file path");
|
|
1143
|
+
gen.config = values.config;
|
|
1144
|
+
}
|
|
1145
|
+
return { command: "generate", generate: gen, json: values.json === true, timeoutMs: DEFAULT_TIMEOUT_MS };
|
|
1146
|
+
}
|
|
985
1147
|
if (command !== "audit") return fail2(`unknown command "${command}"`);
|
|
1148
|
+
const wrong = GENERATE_FLAGS.find((f) => values[f] !== void 0);
|
|
1149
|
+
if (wrong) return fail2(`--${wrong} is an option of generate, not audit`);
|
|
986
1150
|
if (input === void 0) return fail2("audit requires a URL");
|
|
987
1151
|
if (rest.length > 0) return fail2(`unexpected argument "${rest[0]}"`);
|
|
988
1152
|
const args = {
|
|
@@ -1020,15 +1184,20 @@ function parseCliArgs(argv) {
|
|
|
1020
1184
|
return args;
|
|
1021
1185
|
}
|
|
1022
1186
|
|
|
1187
|
+
// src/cli/generate.ts
|
|
1188
|
+
import { existsSync, readFileSync } from "fs";
|
|
1189
|
+
import path from "path";
|
|
1190
|
+
import { pathToFileURL } from "url";
|
|
1191
|
+
|
|
1023
1192
|
// src/cli/terminal.ts
|
|
1024
1193
|
var MAX_TEXT_CHARS = 500;
|
|
1025
1194
|
var CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
1026
1195
|
var BIDI_CHARS = "\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
|
|
1027
1196
|
var BIDI = new RegExp(`[${BIDI_CHARS}]`, "g");
|
|
1028
1197
|
function safeText(s, max = MAX_TEXT_CHARS) {
|
|
1029
|
-
const
|
|
1030
|
-
const chars = Array.from(
|
|
1031
|
-
return chars.length > max ? chars.slice(0, max - 1).join("") + "\u2026" :
|
|
1198
|
+
const clean2 = collapseWhitespace(s.replace(CONTROL, " ").replace(BIDI, ""));
|
|
1199
|
+
const chars = Array.from(clean2);
|
|
1200
|
+
return chars.length > max ? chars.slice(0, max - 1).join("") + "\u2026" : clean2;
|
|
1032
1201
|
}
|
|
1033
1202
|
var JSON_UNSAFE = new RegExp(`[\\u007f-\\u009f\\u2028\\u2029${BIDI_CHARS}]`, "g");
|
|
1034
1203
|
function safeJson(value, space) {
|
|
@@ -1038,6 +1207,75 @@ function safeJson(value, space) {
|
|
|
1038
1207
|
);
|
|
1039
1208
|
}
|
|
1040
1209
|
|
|
1210
|
+
// src/cli/generate.ts
|
|
1211
|
+
var VITE_ENTRY = "@anubhav-choubey/web-for-agents/vite";
|
|
1212
|
+
var clean = (s) => s.split("\n").map((l) => safeText(l, Infinity)).join("\n");
|
|
1213
|
+
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1214
|
+
async function loadConfig(file) {
|
|
1215
|
+
const abs = path.resolve(file);
|
|
1216
|
+
if (!existsSync(abs)) throw new Error(withPrefix(`config file not found: ${abs}`));
|
|
1217
|
+
let config;
|
|
1218
|
+
try {
|
|
1219
|
+
if (abs.endsWith(".json")) config = JSON.parse(readFileSync(abs, "utf8"));
|
|
1220
|
+
else if (/\.m?js$/.test(abs)) config = (await import(pathToFileURL(abs).href)).default;
|
|
1221
|
+
else throw new Error("expected a .json, .js or .mjs file");
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
throw new Error(withPrefix(`could not load config ${abs}: ${errorMessage(e)}`), { cause: e });
|
|
1224
|
+
}
|
|
1225
|
+
if (!isObject2(config)) throw new Error(withPrefix(`config ${abs} must export an options object`));
|
|
1226
|
+
return config;
|
|
1227
|
+
}
|
|
1228
|
+
async function generateOptions(args) {
|
|
1229
|
+
const config = args.config ? await loadConfig(args.config) : {};
|
|
1230
|
+
const robots = isObject2(config.robots) ? config.robots : {};
|
|
1231
|
+
const ai = { ...isObject2(robots.ai) ? robots.ai : {} };
|
|
1232
|
+
if (args.aiTraining) ai.training = args.aiTraining;
|
|
1233
|
+
if (args.aiSearch) ai.search = args.aiSearch;
|
|
1234
|
+
if (ai.training === void 0 || ai.search === void 0) {
|
|
1235
|
+
throw new Error(withPrefix("--ai-training and --ai-search are required (or robots.ai in --config)"));
|
|
1236
|
+
}
|
|
1237
|
+
const siteUrl = args.siteUrl ?? config.siteUrl;
|
|
1238
|
+
if (siteUrl === void 0) throw new Error(withPrefix("--site-url is required (or siteUrl in --config)"));
|
|
1239
|
+
return {
|
|
1240
|
+
...config,
|
|
1241
|
+
outDir: args.outDir,
|
|
1242
|
+
siteUrl,
|
|
1243
|
+
robots: { ...robots, ai },
|
|
1244
|
+
...args.linkMarkdown ? {} : { linkMarkdown: false },
|
|
1245
|
+
...args.overwrite ? { overwrite: true } : {}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
async function runGenerate(args, json, io) {
|
|
1249
|
+
let result;
|
|
1250
|
+
try {
|
|
1251
|
+
const options = await generateOptions(args);
|
|
1252
|
+
const { generateAgentFiles } = await import(VITE_ENTRY);
|
|
1253
|
+
result = await generateAgentFiles({
|
|
1254
|
+
...options,
|
|
1255
|
+
logger: {
|
|
1256
|
+
warn: (m) => io.stderr(clean(m) + "\n"),
|
|
1257
|
+
info: (m) => (json ? io.stderr : io.stdout)(clean(m) + "\n")
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
} catch (e) {
|
|
1261
|
+
io.stderr(withPrefix(`generate failed: ${clean(errorMessage(e).replace(/^\[web-for-agents\] /, ""))}
|
|
1262
|
+
`));
|
|
1263
|
+
return 2;
|
|
1264
|
+
}
|
|
1265
|
+
if (json) {
|
|
1266
|
+
io.stdout(safeJson(result, 2) + "\n");
|
|
1267
|
+
} else {
|
|
1268
|
+
const lines = [
|
|
1269
|
+
`Layout: ${result.fileLayout}`,
|
|
1270
|
+
`Pages: ${result.pages.map((p) => p.route + (p.listed ? "" : " (noindex)")).join(", ")}`,
|
|
1271
|
+
`Wrote ${result.written.length} files: ${result.written.join(", ")}`
|
|
1272
|
+
];
|
|
1273
|
+
if (result.kept.length > 0) lines.push(`Kept existing: ${result.kept.join(", ")} (use --overwrite to replace)`);
|
|
1274
|
+
io.stdout(clean(lines.join("\n")) + "\n");
|
|
1275
|
+
}
|
|
1276
|
+
return 0;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1041
1279
|
// src/cli/report.ts
|
|
1042
1280
|
function formatText(report) {
|
|
1043
1281
|
const lines = [
|
|
@@ -1077,6 +1315,7 @@ ${USAGE}`);
|
|
|
1077
1315
|
io.stdout(USAGE);
|
|
1078
1316
|
return 0;
|
|
1079
1317
|
}
|
|
1318
|
+
if (args.command === "generate") return runGenerate(args.generate, args.json, io);
|
|
1080
1319
|
const report = await audit(args.url, {
|
|
1081
1320
|
timeoutMs: args.timeoutMs,
|
|
1082
1321
|
...args.agentMap === void 0 ? {} : { agentMapPath: args.agentMap },
|