@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/dist/vite.js CHANGED
@@ -1,23 +1,35 @@
1
1
  import {
2
- AI_BOTS
3
- } from "./chunk-CKXHTGGT.js";
2
+ EMPTY_OBJECT_SCHEMA,
3
+ schemaFromForm
4
+ } from "./chunk-OKCYZE6C.js";
4
5
  import {
5
- AGENT_MAP_VERSION
6
- } from "./chunk-LCVI2PRP.js";
6
+ AI_BOTS,
7
+ scanHtml
8
+ } from "./chunk-CGIIB6AF.js";
9
+ import {
10
+ metaLengthWarnings
11
+ } from "./chunk-L3AYWCSX.js";
7
12
  import {
8
13
  DEFAULT_CONTENT_SELECTOR,
9
14
  DEFAULT_SHORTCUT,
10
15
  buildPageMarkdown,
11
16
  escapeLinkTarget,
12
17
  escapeMarkdownInline,
18
+ firstPerName,
19
+ linkActions,
20
+ markupActions,
21
+ markupLinks,
13
22
  parseShortcut,
14
23
  readPageMeta,
15
24
  selectContentRegion
16
- } from "./chunk-ZYMNO5AO.js";
25
+ } from "./chunk-T42S3WRV.js";
17
26
  import {
18
27
  compareStrings
19
28
  } from "./chunk-AAIYCJD3.js";
20
- import "./chunk-FEAKVNMG.js";
29
+ import {
30
+ AGENT_MAP_VERSION,
31
+ isValidActionName
32
+ } from "./chunk-4STTPCBR.js";
21
33
  import {
22
34
  IDLE_MS,
23
35
  PROBE_ATTR,
@@ -25,13 +37,19 @@ import {
25
37
  TEMP_BASE_ATTR,
26
38
  prepareShell,
27
39
  readyCheck
28
- } from "./chunk-HONWT6GK.js";
40
+ } from "./chunk-K3B53RIO.js";
29
41
  import {
42
+ ACTION_ATTR,
43
+ ACTION_DESCRIPTION_ATTR,
44
+ ACTION_DESTRUCTIVE_ATTR,
30
45
  AGENT_GLOBAL,
31
46
  AGENT_MAP_PATH,
32
47
  CONTENT_ATTR,
48
+ HEAD_OWNED_ATTR,
49
+ LINK_ACTION_SELECTOR,
33
50
  LLMS_FULL_TXT_PATH,
34
51
  LLMS_TXT_PATH,
52
+ MARKDOWN_LINK_KEY,
35
53
  MARKDOWN_MIME,
36
54
  PRERENDERED_ATTR,
37
55
  ROBOTS_PATH,
@@ -41,20 +59,25 @@ import {
41
59
  mountPath,
42
60
  normalizeRoute,
43
61
  publicUrl,
44
- trimTrailingSlash
45
- } from "./chunk-AZG5MLNG.js";
62
+ trimTrailingSlash,
63
+ tryResolveUrl
64
+ } from "./chunk-2Y3LLYEC.js";
46
65
  import {
47
66
  POLL_MS,
48
67
  RenderError,
49
68
  SERIALIZE_EXPR,
69
+ SNAPSHOT_GRACE_MS,
70
+ TimeoutError,
71
+ assertNoPageErrors,
50
72
  blockedHint,
51
73
  createDeadline,
52
74
  createOriginPolicy,
53
75
  readyTimeoutMessage,
54
76
  runInPage,
77
+ snapshotWarning,
55
78
  waitForReady,
56
79
  withoutBlockedLoads
57
- } from "./chunk-5TNG4QMP.js";
80
+ } from "./chunk-PGZH33B5.js";
58
81
  import {
59
82
  LOG_PREFIX,
60
83
  errorMessage,
@@ -77,9 +100,18 @@ function segments(route) {
77
100
  function decodeSegments(route) {
78
101
  return segments(normalizeRoute(route)).map((s) => decodeURIComponent(s));
79
102
  }
80
- function routeHtmlFile(route) {
103
+ function encodePathSegment(segment) {
104
+ return encodeURIComponent(segment).replace(/%(24|26|2B|2C|3B|3D|3A|40)/g, (m) => decodeURIComponent(m));
105
+ }
106
+ function routeOfHtmlFile(file) {
107
+ const segs = file.slice(0, -".html".length).split("/");
108
+ if (segs[segs.length - 1] === "index") segs.pop();
109
+ return "/" + segs.map(encodePathSegment).join("/");
110
+ }
111
+ function routeHtmlFile(route, layout = "directory") {
81
112
  const segs = decodeSegments(route);
82
- return segs.length === 0 ? "index.html" : segs.join("/") + "/index.html";
113
+ if (segs.length === 0) return "index.html";
114
+ return segs.join("/") + (layout === "flat" ? ".html" : "/index.html");
83
115
  }
84
116
  function routeMdFile(route) {
85
117
  return markdownPathFor(route).slice(1).split("/").map((s) => decodeURIComponent(s)).join("/");
@@ -108,7 +140,7 @@ function isSafeRoute(route) {
108
140
  var MAX_NAME_BYTES = 255;
109
141
  var MAX_SEGMENT_BYTES = MAX_NAME_BYTES - ".md".length;
110
142
  var MAX_ROUTE_BYTES = 1024;
111
- function isRouteTooLong(route) {
143
+ function isRouteTooLong(route, layout = "directory") {
112
144
  let segs;
113
145
  try {
114
146
  segs = decodeSegments(route);
@@ -116,9 +148,10 @@ function isRouteTooLong(route) {
116
148
  return false;
117
149
  }
118
150
  let total = 0;
119
- for (const s of segs) {
151
+ const leafMax = layout === "flat" ? MAX_NAME_BYTES - ".html".length : MAX_SEGMENT_BYTES;
152
+ for (const [i, s] of segs.entries()) {
120
153
  const n = Buffer.byteLength(s, "utf8");
121
- if (n > MAX_SEGMENT_BYTES) return true;
154
+ if (n > (i === segs.length - 1 ? leafMax : MAX_SEGMENT_BYTES)) return true;
122
155
  total += n + 1;
123
156
  }
124
157
  return total > MAX_ROUTE_BYTES;
@@ -136,16 +169,16 @@ function linkToRoute(href, origin, base) {
136
169
  if (u.protocol !== "http:" && u.protocol !== "https:") return null;
137
170
  if (u.origin !== new URL(origin).origin) return null;
138
171
  const b = trimTrailingSlash(base);
139
- let path5 = u.pathname;
172
+ let path6 = u.pathname;
140
173
  if (b !== "") {
141
- if (path5 !== b && !path5.startsWith(b + "/")) return null;
142
- path5 = path5.slice(b.length);
174
+ if (path6 !== b && !path6.startsWith(b + "/")) return null;
175
+ path6 = path6.slice(b.length);
143
176
  }
144
- if (path5 === "") path5 = "/";
145
- path5 = path5.replace(/\/index\.html$/, "") || "/";
146
- const last = path5.split("/").pop();
177
+ if (path6 === "") path6 = "/";
178
+ path6 = path6.replace(/\/index\.html$/, "") || "/";
179
+ const last = path6.split("/").pop();
147
180
  if (/\.[a-z0-9]+$/i.test(last)) return null;
148
- const route = normalizeRoute(path5);
181
+ const route = normalizeRoute(path6);
149
182
  return isSafeRoute(route) ? route : null;
150
183
  }
151
184
 
@@ -155,14 +188,32 @@ var WEBMCP_INFO = {
155
188
  api: "document.modelContext",
156
189
  spec: "https://webmachinelearning.github.io/webmcp/"
157
190
  };
158
- function collectAgentPageInPage(globalKey) {
159
- const api = window[globalKey];
160
- return { actions: api && typeof api.actions === "function" ? api.actions() : [] };
191
+ var AGENT_PAGE_IN_PAGE_ARG = {
192
+ globalKey: AGENT_GLOBAL,
193
+ linkSelector: LINK_ACTION_SELECTOR,
194
+ nameAttr: ACTION_ATTR,
195
+ descriptionAttr: ACTION_DESCRIPTION_ATTR,
196
+ destructiveAttr: ACTION_DESTRUCTIVE_ATTR
197
+ };
198
+ function collectAgentPageInPage(arg) {
199
+ const api = window[arg.globalKey];
200
+ const actions = api && typeof api.actions === "function" ? api.actions() : [];
201
+ const links = [];
202
+ document.querySelectorAll(arg.linkSelector).forEach((el) => {
203
+ const name = (el.getAttribute(arg.nameAttr) || "").trim();
204
+ if (!name) return;
205
+ links.push({
206
+ name,
207
+ description: el.getAttribute(arg.descriptionAttr) || null,
208
+ destructive: el.getAttribute(arg.destructiveAttr) === "true",
209
+ href: el.getAttribute("href") || ""
210
+ });
211
+ });
212
+ return { actions, links };
161
213
  }
162
214
  var byName = (a, b) => compareStrings(a.name, b.name);
163
215
  function toPage(siteUrl, p) {
164
- const seen = /* @__PURE__ */ new Set();
165
- const actions = p.actions.filter((a) => !seen.has(a.name) && !!seen.add(a.name)).sort(byName);
216
+ const actions = firstPerName(p.actions).sort(byName);
166
217
  return {
167
218
  path: p.route,
168
219
  url: publicUrl(siteUrl, p.route),
@@ -199,14 +250,21 @@ function agentMapExtension(opts) {
199
250
  return {
200
251
  name: "agent-map",
201
252
  inPage: collectAgentPageInPage,
202
- inPageArg: AGENT_GLOBAL,
253
+ inPageArg: AGENT_PAGE_IN_PAGE_ARG,
203
254
  onRoute(route, page) {
204
255
  data.set(route, page);
205
256
  },
206
257
  llmsSections: () => [agentMapLlmsSection(siteUrl, fileName)],
207
258
  finalize(ctx) {
208
259
  const pages = ctx.pages.map((p) => {
209
- const page = { route: p.route, actions: data.get(p.route)?.actions ?? [] };
260
+ const found = data.get(p.route);
261
+ const actions = found?.actions ?? [];
262
+ const links = linkActions(
263
+ found?.links ?? [],
264
+ { pageUrl: p.url, page: p.route, taken: new Set(actions.map((a) => a.name)) },
265
+ ctx.warn
266
+ );
267
+ const page = { route: p.route, actions: [...actions, ...links] };
210
268
  if (p.title) page.title = p.title;
211
269
  if (p.description) page.description = p.description;
212
270
  if (p.mdUrl) page.markdown = p.mdUrl;
@@ -231,6 +289,21 @@ function outputFileName(fileName) {
231
289
  }
232
290
  return rel;
233
291
  }
292
+ function symlinkOnPath(outDir, rel) {
293
+ let p = outDir;
294
+ const parts = rel.split("/");
295
+ for (let i = 0; i < parts.length; i++) {
296
+ p = path.join(p, parts[i]);
297
+ let st;
298
+ try {
299
+ st = fs.lstatSync(p);
300
+ } catch {
301
+ return null;
302
+ }
303
+ if (st.isSymbolicLink()) return parts.slice(0, i + 1).join("/");
304
+ }
305
+ return null;
306
+ }
234
307
  var short = (s, max) => s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
235
308
  function createFileStager(opts) {
236
309
  const files = /* @__PURE__ */ new Map();
@@ -247,6 +320,14 @@ function createFileStager(opts) {
247
320
  stage(fileName, source) {
248
321
  const file = outputFileName(fileName);
249
322
  if (inPublicDir(file)) return { file, status: "skipped" };
323
+ const link = symlinkOnPath(opts.outDir, file);
324
+ if (link !== null) {
325
+ if (!warned.has("link:" + file)) {
326
+ warned.add("link:" + file);
327
+ opts.warn(`${LOG_PREFIX} ${link} is a symbolic link; not writing ${file} through it`);
328
+ }
329
+ return { file, status: "skipped" };
330
+ }
250
331
  const status = files.has(file) ? "replaced" : "staged";
251
332
  files.set(file, source);
252
333
  return { file, status };
@@ -299,34 +380,6 @@ function createFileStager(opts) {
299
380
  };
300
381
  }
301
382
 
302
- // src/vite/llms.ts
303
- function header(site) {
304
- const blocks = [`# ${escapeMarkdownInline(site.title)}`];
305
- const desc = escapeMarkdownInline(site.description);
306
- if (desc) blocks.push(`> ${desc}`);
307
- return blocks;
308
- }
309
- function buildLlmsTxt(site, pages, sections = []) {
310
- const blocks = header(site);
311
- const lines = [...pages].sort(byRoute).map((p) => {
312
- const title = escapeMarkdownInline(p.title);
313
- const desc = escapeMarkdownInline(p.description);
314
- return `- [${title}](${escapeLinkTarget(p.mdUrl ?? p.url)})${desc ? `: ${desc}` : ""}`;
315
- });
316
- blocks.push(["## Pages", ...lines.length ? ["", ...lines] : []].join("\n"));
317
- for (const s of sections) {
318
- blocks.push([`## ${s.heading}`, ...s.lines.length ? ["", ...s.lines] : []].join("\n"));
319
- }
320
- return blocks.join("\n\n") + "\n";
321
- }
322
- function buildLlmsFullTxt(site, pages) {
323
- const head = header(site).join("\n\n");
324
- const blocks = [...pages].sort(byRoute).filter((p) => p.markdown !== void 0).map((p) => `Source: ${p.url}
325
-
326
- ${p.markdown.trim()}`);
327
- return [head, blocks.join("\n\n---\n\n")].filter(Boolean).join("\n\n") + "\n";
328
- }
329
-
330
383
  // src/vite/prerender/happyDomProcess.ts
331
384
  import { fork } from "child_process";
332
385
  import { createHash } from "crypto";
@@ -352,7 +405,7 @@ function nodeModulesRoot(file) {
352
405
  }
353
406
  var sourceEntry;
354
407
  async function childEntryFile() {
355
- const { CHILD_ENTRY } = await import("./renderChild-Y2RFSYY4.js");
408
+ const { CHILD_ENTRY } = await import("./renderChild-PSQLAIUW.js");
356
409
  const file = fileURLToPath(CHILD_ENTRY);
357
410
  if (!file.endsWith(".ts")) return file;
358
411
  return sourceEntry ??= bundleFromSource(file);
@@ -732,8 +785,18 @@ function resolvePrerender(input) {
732
785
  concurrency: posInt("concurrency", input.concurrency, usesBrowser ? 4 : 8),
733
786
  maxRoutes: posInt("maxRoutes", input.maxRoutes, 500),
734
787
  allowOrigins,
735
- isolation: resolveIsolation(input.isolation, bad)
788
+ isolation: resolveIsolation(input.isolation, bad),
789
+ fileLayout: input.fileLayout ?? "directory",
790
+ onTimeout: input.onTimeout ?? "fail"
736
791
  };
792
+ if (out.fileLayout !== "directory" && out.fileLayout !== "flat") bad("fileLayout", '"directory" or "flat"');
793
+ if (out.onTimeout !== "fail" && out.onTimeout !== "snapshot") bad("onTimeout", '"fail" or "snapshot"');
794
+ if (input.readySelector !== void 0) {
795
+ if (typeof input.readySelector !== "string" || input.readySelector.trim() === "") {
796
+ bad("readySelector", "a non-empty string");
797
+ }
798
+ out.readySelector = input.readySelector;
799
+ }
737
800
  if (input.executablePath !== void 0) out.executablePath = input.executablePath;
738
801
  if (input.idleMs !== void 0) {
739
802
  if (typeof input.idleMs !== "number" || !Number.isInteger(input.idleMs) || input.idleMs < 0) {
@@ -751,6 +814,90 @@ function agentOutputs(r) {
751
814
  if (r.agentMap) out.push(r.agentMap.fileName);
752
815
  return out;
753
816
  }
817
+ function resolveMarkdown(input) {
818
+ if (input !== void 0 && (typeof input !== "object" || input === null)) {
819
+ throw new Error(withPrefix("markdown must be false or { contentSelector, transform }"));
820
+ }
821
+ const sel = input?.contentSelector;
822
+ if (sel !== void 0 && (typeof sel !== "string" || sel === "")) {
823
+ throw new Error(`${LOG_PREFIX} markdown.contentSelector must be a non-empty string`);
824
+ }
825
+ const out = { contentSelector: sel ?? DEFAULT_CONTENT_SELECTOR };
826
+ if (input?.transform !== void 0) {
827
+ if (typeof input.transform !== "function") throw new Error(withPrefix("markdown.transform must be a function"));
828
+ out.transform = input.transform;
829
+ }
830
+ return out;
831
+ }
832
+ function resolveSitemap(input) {
833
+ if (input === void 0 || typeof input === "boolean") return { sitemap: input ?? true, sitemapLastmod: "none" };
834
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
835
+ throw new Error(withPrefix("sitemap must be a boolean or { lastmod }"));
836
+ }
837
+ const lastmod = input.lastmod ?? "none";
838
+ if (lastmod !== "file" && lastmod !== "none" && typeof lastmod !== "function") {
839
+ throw new Error(withPrefix('sitemap.lastmod must be "file", "none" or a function'));
840
+ }
841
+ return { sitemap: true, sitemapLastmod: lastmod };
842
+ }
843
+ function resolveLlms(input, siteUrl) {
844
+ if (input === void 0) return {};
845
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
846
+ throw new Error(withPrefix("llms must be false or { title, description, intro, sections }"));
847
+ }
848
+ const str = (v, field, nonEmpty = false) => {
849
+ if (v === void 0) return void 0;
850
+ if (typeof v !== "string" || nonEmpty && v.trim() === "") {
851
+ throw new Error(withPrefix(`llms.${field} must be a ${nonEmpty ? "non-empty " : ""}string`));
852
+ }
853
+ return v;
854
+ };
855
+ const out = { ...input };
856
+ str(input.title, "title");
857
+ str(input.description, "description");
858
+ str(input.intro, "intro");
859
+ if (input.sections === void 0) return out;
860
+ if (!Array.isArray(input.sections)) throw new Error(withPrefix("llms.sections must be an array"));
861
+ out.sections = input.sections.map((sec, i) => {
862
+ const at = `sections[${i}]`;
863
+ if (typeof sec !== "object" || sec === null || Array.isArray(sec)) {
864
+ throw new Error(withPrefix(`llms.${at} must be an object such as { title, text, links }`));
865
+ }
866
+ const { title, text, links } = sec;
867
+ if (title === void 0) throw new Error(withPrefix(`llms.${at}.title is required`));
868
+ const s = { title: str(title, `${at}.title`, true) };
869
+ const t = str(text, `${at}.text`);
870
+ if (t !== void 0) s.text = t;
871
+ if (links !== void 0) {
872
+ if (!Array.isArray(links)) throw new Error(withPrefix(`llms.${at}.links must be an array`));
873
+ s.links = links.map((l, j) => {
874
+ const lat = `${at}.links[${j}]`;
875
+ if (typeof l !== "object" || l === null || Array.isArray(l)) {
876
+ throw new Error(withPrefix(`llms.${lat} must be an object such as { title, url, description }`));
877
+ }
878
+ const link = l;
879
+ if (link.title === void 0) throw new Error(withPrefix(`llms.${lat}.title is required`));
880
+ if (link.url === void 0) throw new Error(withPrefix(`llms.${lat}.url is required`));
881
+ const url = typeof link.url === "string" ? tryResolveUrl(link.url.trim(), siteUrl + "/") : null;
882
+ const scheme = url === null ? "" : new URL(url).protocol;
883
+ if (url === null || !["http:", "https:", "mailto:"].includes(scheme)) {
884
+ throw new Error(
885
+ withPrefix(`llms.${lat}.url must be an http(s) or mailto: URL, or a path on the site (got ${JSON.stringify(link.url)})`)
886
+ );
887
+ }
888
+ const o = { title: str(link.title, `${lat}.title`, true), url };
889
+ const d = str(link.description, `${lat}.description`);
890
+ if (d !== void 0) o.description = d;
891
+ return o;
892
+ });
893
+ }
894
+ if (!s.text?.trim() && !s.links?.length) {
895
+ throw new Error(withPrefix(`llms.${at} needs text or links`));
896
+ }
897
+ return s;
898
+ });
899
+ return out;
900
+ }
754
901
  var AGENT_MAP_FILE = /^[A-Za-z0-9_.-]+\.json$/;
755
902
  function resolveAgentMap(input) {
756
903
  if (input === false) return false;
@@ -814,8 +961,8 @@ function resolveOptions(input) {
814
961
  throw new Error(`${LOG_PREFIX} robots.ai.overrides.${bot} must be "allow" or "block"`);
815
962
  }
816
963
  }
817
- for (const path5 of input.robots.disallow ?? []) {
818
- if (/[\r\n]/.test(path5)) {
964
+ for (const path6 of input.robots.disallow ?? []) {
965
+ if (/[\r\n]/.test(path6)) {
819
966
  throw new Error(`${LOG_PREFIX} robots.disallow paths must not contain line breaks`);
820
967
  }
821
968
  }
@@ -826,6 +973,7 @@ function resolveOptions(input) {
826
973
  const prerender = input.prerender === false ? false : resolvePrerender(input.prerender ?? {});
827
974
  let markdown = false;
828
975
  let llms = false;
976
+ const requestedLlms = input.llms === false ? false : resolveLlms(input.llms, siteUrl);
829
977
  const requestedAgentMap = resolveAgentMap(input.agentMap);
830
978
  let agentMap = false;
831
979
  if (prerender === false) {
@@ -836,21 +984,15 @@ function resolveOptions(input) {
836
984
  warnings.push(withPrefix(`${names} ${verb} disabled because prerender is false`));
837
985
  }
838
986
  } else {
839
- if (input.markdown !== false) {
840
- const sel = input.markdown?.contentSelector;
841
- if (sel !== void 0 && (typeof sel !== "string" || sel === "")) {
842
- throw new Error(`${LOG_PREFIX} markdown.contentSelector must be a non-empty string`);
843
- }
844
- markdown = { contentSelector: sel ?? DEFAULT_CONTENT_SELECTOR };
845
- }
846
- if (input.llms !== false) llms = { ...input.llms };
987
+ if (input.markdown !== false) markdown = resolveMarkdown(input.markdown);
988
+ llms = requestedLlms;
847
989
  agentMap = requestedAgentMap;
848
990
  }
849
991
  const resolved = {
850
992
  siteUrl,
851
993
  routes,
852
994
  robots: input.robots,
853
- sitemap: input.sitemap ?? true,
995
+ ...resolveSitemap(input.sitemap),
854
996
  exclude,
855
997
  prerender,
856
998
  markdown,
@@ -883,9 +1025,9 @@ function parseHtml(html) {
883
1025
  handleDisabledFileLoadingAsSuccess: true
884
1026
  }
885
1027
  });
886
- const document = new window2.DOMParser().parseFromString(html, "text/html");
1028
+ const document2 = new window2.DOMParser().parseFromString(html, "text/html");
887
1029
  return {
888
- document,
1030
+ document: document2,
889
1031
  dispose() {
890
1032
  window2.happyDOM.close();
891
1033
  }
@@ -898,9 +1040,9 @@ var SHOW_TEXT = 4;
898
1040
  var FILTER_ACCEPT = 1;
899
1041
  var FILTER_REJECT = 2;
900
1042
  var NO_TEXT = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
901
- function bodyHasText(document) {
902
- if (!document.body) return false;
903
- const walker = document.createTreeWalker(document.body, SHOW_ELEMENT | SHOW_TEXT, {
1043
+ function bodyHasText(document2) {
1044
+ if (!document2.body) return false;
1045
+ const walker = document2.createTreeWalker(document2.body, SHOW_ELEMENT | SHOW_TEXT, {
904
1046
  acceptNode: (n) => n.nodeType === 1 && NO_TEXT.has(n.tagName.toUpperCase()) ? FILTER_REJECT : FILTER_ACCEPT
905
1047
  });
906
1048
  for (let n = walker.nextNode(); n; n = walker.nextNode()) {
@@ -908,11 +1050,11 @@ function bodyHasText(document) {
908
1050
  }
909
1051
  return false;
910
1052
  }
911
- function analyzeDocument(document, opts, mode = {}) {
912
- const meta = readPageMeta(document);
1053
+ function analyzeDocument(document2, opts, mode = {}) {
1054
+ const meta = readPageMeta(document2);
913
1055
  const title = meta.title ?? opts.route;
914
1056
  const links = [];
915
- document.querySelectorAll("a[href]").forEach((a) => {
1057
+ document2.querySelectorAll("a[href]").forEach((a) => {
916
1058
  if (a.hasAttribute("download")) return;
917
1059
  try {
918
1060
  const u = new URL(a.getAttribute("href"), opts.pageUrl);
@@ -920,8 +1062,8 @@ function analyzeDocument(document, opts, mode = {}) {
920
1062
  } catch {
921
1063
  }
922
1064
  });
923
- const hasText = bodyHasText(document);
924
- const { element, source } = selectContentRegion(document, {
1065
+ const hasText = bodyHasText(document2);
1066
+ const { element, source } = selectContentRegion(document2, {
925
1067
  contentSelector: opts.contentSelector,
926
1068
  ...mode.inPlace ? { inPlace: true } : {}
927
1069
  });
@@ -951,9 +1093,9 @@ var URL_ATTRS = [
951
1093
  function mapSrcset(value, fn) {
952
1094
  return value.replace(/(^|,)(\s*)([^\s,][^\s]*?)(?=,|\s|$)/g, (_m, sep2, ws, url) => sep2 + ws + fn(url));
953
1095
  }
954
- function rewriteUrls(document, fn, attrs = URL_ATTRS) {
1096
+ function rewriteUrls(document2, fn, attrs = URL_ATTRS) {
955
1097
  for (const [tag, attr] of attrs) {
956
- document.querySelectorAll(`${tag}[${attr}]`).forEach((el) => {
1098
+ document2.querySelectorAll(`${tag}[${attr}]`).forEach((el) => {
957
1099
  const v = el.getAttribute(attr);
958
1100
  const next = attr === "srcset" ? mapSrcset(v, fn) : fn(v);
959
1101
  if (next !== v) el.setAttribute(attr, next);
@@ -961,18 +1103,18 @@ function rewriteUrls(document, fn, attrs = URL_ATTRS) {
961
1103
  }
962
1104
  }
963
1105
  function finalizePage(html, opts, analyze) {
964
- const { document, dispose } = parseHtml(html);
1106
+ const { document: document2, dispose } = parseHtml(html);
965
1107
  try {
966
- const out = finalizeDocument(document, opts);
967
- return { html: out, info: analyzeDocument(document, analyze, { inPlace: true }) };
1108
+ const out = finalizeDocument(document2, opts);
1109
+ return { html: out, info: analyzeDocument(document2, analyze, { inPlace: true }) };
968
1110
  } finally {
969
1111
  dispose();
970
1112
  }
971
1113
  }
972
- function finalizeDocument(document, opts) {
973
- document.querySelectorAll(`[${PROBE_ATTR}], [${TEMP_BASE_ATTR}]`).forEach((el) => el.remove());
974
- document.documentElement.setAttribute(PRERENDERED_ATTR, "");
975
- document.querySelectorAll(`script[${REL_SRC_ATTR}]`).forEach((el) => {
1114
+ function finalizeDocument(document2, opts) {
1115
+ document2.querySelectorAll(`[${PROBE_ATTR}], [${TEMP_BASE_ATTR}]`).forEach((el) => el.remove());
1116
+ document2.documentElement.setAttribute(PRERENDERED_ATTR, "");
1117
+ document2.querySelectorAll(`script[${REL_SRC_ATTR}]`).forEach((el) => {
976
1118
  const src = el.getAttribute("src");
977
1119
  if (src?.startsWith("/")) el.setAttribute("src", "." + src);
978
1120
  el.removeAttribute(REL_SRC_ATTR);
@@ -980,24 +1122,24 @@ function finalizeDocument(document, opts) {
980
1122
  if (opts.origin) {
981
1123
  const origin = trimTrailingSlash(opts.origin);
982
1124
  const attrs = opts.fullBase ? [...URL_ATTRS, ["a", "href"]] : URL_ATTRS;
983
- rewriteUrls(document, (u) => {
1125
+ rewriteUrls(document2, (u) => {
984
1126
  if (!u.startsWith(origin)) return u;
985
1127
  const rest = u.slice(origin.length);
986
1128
  if (rest !== "" && !/^[/?#]/.test(rest)) return u;
987
- const path5 = rest.startsWith("/") ? rest : "/" + rest;
988
- if (opts.fullBase) return trimTrailingSlash(opts.fullBase) + "/" + path5.slice(1);
989
- return opts.relativeBase ? "." + path5 : path5;
1129
+ const path6 = rest.startsWith("/") ? rest : "/" + rest;
1130
+ if (opts.fullBase) return trimTrailingSlash(opts.fullBase) + "/" + path6.slice(1);
1131
+ return opts.relativeBase ? "." + path6 : path6;
990
1132
  }, attrs);
991
1133
  }
992
1134
  if (opts.relativeBase) {
993
1135
  const depth = routeDepth(opts.route);
994
1136
  if (depth > 0) {
995
1137
  const up = "../".repeat(depth);
996
- rewriteUrls(document, (u) => u.startsWith("./") ? up + u.slice(2) : u);
1138
+ rewriteUrls(document2, (u) => u.startsWith("./") ? up + u.slice(2) : u);
997
1139
  }
998
1140
  }
999
1141
  const seen = /* @__PURE__ */ new Set();
1000
- document.querySelectorAll("link[href]").forEach((el) => {
1142
+ document2.querySelectorAll("link[href]").forEach((el) => {
1001
1143
  const rel = el.getAttribute("rel") ?? "";
1002
1144
  if (!DEDUPE_RELS.has(rel)) return;
1003
1145
  const key = [rel, el.getAttribute("href"), el.getAttribute("as"), el.getAttribute("media"), el.getAttribute("type")].join("\n");
@@ -1005,16 +1147,16 @@ function finalizeDocument(document, opts) {
1005
1147
  else seen.add(key);
1006
1148
  });
1007
1149
  if (opts.markdownUrl) {
1008
- let link = document.head.querySelector(`link[rel="alternate"][type="${MARKDOWN_MIME}"]`);
1150
+ let link = document2.head.querySelector(`link[rel="alternate"][type="${MARKDOWN_MIME}"]`);
1009
1151
  if (!link) {
1010
- link = document.createElement("link");
1152
+ link = document2.createElement("link");
1011
1153
  link.setAttribute("rel", "alternate");
1012
1154
  link.setAttribute("type", MARKDOWN_MIME);
1013
- document.head.appendChild(link);
1155
+ document2.head.appendChild(link);
1014
1156
  }
1015
1157
  link.setAttribute("href", opts.markdownUrl);
1016
1158
  }
1017
- return "<!DOCTYPE html>" + document.documentElement.outerHTML;
1159
+ return "<!DOCTYPE html>" + document2.documentElement.outerHTML;
1018
1160
  }
1019
1161
 
1020
1162
  // src/vite/prerender/crawl.ts
@@ -1050,7 +1192,7 @@ async function crawlRoutes(opts, render) {
1050
1192
  const seeds = [];
1051
1193
  for (const s of [...new Set(opts.seeds.map(normalizeRoute))].sort()) {
1052
1194
  if (!isSafeRoute(s)) continue;
1053
- if (isRouteTooLong(s)) seedTooLong.push(s);
1195
+ if (isRouteTooLong(s, opts.fileLayout)) seedTooLong.push(s);
1054
1196
  else if (!skip(s)) seeds.push(s);
1055
1197
  }
1056
1198
  const attempt = async (route, renderer) => {
@@ -1104,7 +1246,7 @@ async function crawlRoutes(opts, render) {
1104
1246
  const tooLong2 = [];
1105
1247
  const candidate = (r) => {
1106
1248
  if (r === null || isExcluded(r, opts.exclude)) return false;
1107
- if (isRouteTooLong(r)) {
1249
+ if (isRouteTooLong(r, opts.fileLayout)) {
1108
1250
  tooLong2.push(r);
1109
1251
  return false;
1110
1252
  }
@@ -1286,7 +1428,7 @@ async function createChromeRenderer(channel, opts = {}, load = () => import("pla
1286
1428
  }
1287
1429
  const allowed = createOriginPolicy(opts.allowOrigins);
1288
1430
  const idleMs = opts.idleMs ?? IDLE_MS;
1289
- const check = readyCheck(idleMs);
1431
+ const check = readyCheck(idleMs, opts.readySelector);
1290
1432
  let gone = false;
1291
1433
  browser.on?.("disconnected", () => {
1292
1434
  gone = true;
@@ -1303,8 +1445,9 @@ async function createChromeRenderer(channel, opts = {}, load = () => import("pla
1303
1445
  return e;
1304
1446
  };
1305
1447
  const renderPage = async (url, timeoutMs, inPage) => {
1306
- const deadline = createDeadline(timeoutMs);
1307
- const timeoutText = readyTimeoutMessage(timeoutMs);
1448
+ let deadline = createDeadline(timeoutMs);
1449
+ let timeoutText = readyTimeoutMessage(timeoutMs);
1450
+ let budgetMs = timeoutMs;
1308
1451
  const origin = new URL(url).origin;
1309
1452
  const blocked = [];
1310
1453
  const opening = browser.newPage({ serviceWorkers: "block" }).then(async (p) => {
@@ -1345,22 +1488,35 @@ async function createChromeRenderer(channel, opts = {}, load = () => import("pla
1345
1488
  page.on("pageerror", (e) => pageErrors.push(e.message));
1346
1489
  let html;
1347
1490
  let finalPath;
1491
+ let timedOut = false;
1348
1492
  try {
1349
1493
  await deadline.race(
1350
1494
  page.goto(url, { waitUntil: "load", timeout: deadline.remaining() + 1e3 }),
1351
1495
  timeoutText
1352
1496
  );
1353
- await deadline.race(
1354
- waitForReady(
1355
- async () => {
1356
- const r = await page.evaluate(check);
1357
- return { ready: r.ready, errors: withoutBlockedLoads([...pageErrors, ...r.errors], blocked) };
1358
- },
1359
- timeoutMs,
1360
- Math.min(POLL_MS, idleMs)
1361
- ),
1362
- timeoutText
1363
- );
1497
+ try {
1498
+ await deadline.race(
1499
+ waitForReady(
1500
+ async () => {
1501
+ const r = await page.evaluate(check);
1502
+ return { ready: r.ready, errors: withoutBlockedLoads([...pageErrors, ...r.errors], blocked) };
1503
+ },
1504
+ timeoutMs,
1505
+ Math.min(POLL_MS, idleMs)
1506
+ ),
1507
+ timeoutText
1508
+ );
1509
+ } catch (e) {
1510
+ if (!(opts.onTimeout === "snapshot" && e instanceof TimeoutError)) throw e;
1511
+ timedOut = true;
1512
+ deadline = createDeadline(SNAPSHOT_GRACE_MS);
1513
+ budgetMs = SNAPSHOT_GRACE_MS;
1514
+ timeoutText = readyTimeoutMessage(SNAPSHOT_GRACE_MS);
1515
+ await assertNoPageErrors(async () => {
1516
+ const r = await deadline.race(page.evaluate(check), timeoutText);
1517
+ return withoutBlockedLoads([...pageErrors, ...r.errors], blocked);
1518
+ });
1519
+ }
1364
1520
  html = await deadline.race(page.evaluate(SERIALIZE_EXPR), timeoutText);
1365
1521
  finalPath = new URL(page.url()).pathname;
1366
1522
  } catch (e) {
@@ -1371,9 +1527,9 @@ async function createChromeRenderer(channel, opts = {}, load = () => import("pla
1371
1527
  inPage,
1372
1528
  (expr) => page.evaluate(expr),
1373
1529
  deadline,
1374
- timeoutMs
1530
+ budgetMs
1375
1531
  );
1376
- return { html, finalPath, inPage: results };
1532
+ return { html, finalPath, inPage: results, ...timedOut ? { warnings: [snapshotWarning(timeoutMs)] } : {} };
1377
1533
  } finally {
1378
1534
  await settleWithin(page.close(), opts.pageCloseMs ?? PAGE_CLOSE_MS);
1379
1535
  }
@@ -1397,6 +1553,10 @@ async function createChromeRenderer(channel, opts = {}, load = () => import("pla
1397
1553
  // src/vite/prerender/renderers.ts
1398
1554
  function createRendererPool(opts) {
1399
1555
  const made = /* @__PURE__ */ new Map();
1556
+ const readiness = {
1557
+ ...opts.readySelector !== void 0 ? { readySelector: opts.readySelector } : {},
1558
+ ...opts.onTimeout !== void 0 ? { onTimeout: opts.onTimeout } : {}
1559
+ };
1400
1560
  return {
1401
1561
  get(name) {
1402
1562
  let r = made.get(name);
@@ -1406,6 +1566,7 @@ function createRendererPool(opts) {
1406
1566
  {
1407
1567
  allowOrigins: opts.allowOrigins ?? [],
1408
1568
  ...opts.idleMs !== void 0 ? { idleMs: opts.idleMs } : {},
1569
+ ...readiness,
1409
1570
  ...opts.syncResources ? { syncResources: opts.syncResources } : {}
1410
1571
  },
1411
1572
  { codeGeneration: opts.codeGeneration === true }
@@ -1413,7 +1574,8 @@ function createRendererPool(opts) {
1413
1574
  ) : createChromeRenderer(name, {
1414
1575
  ...opts.executablePath !== void 0 ? { executablePath: opts.executablePath } : {},
1415
1576
  allowOrigins: opts.allowOrigins ?? [],
1416
- ...opts.idleMs !== void 0 ? { idleMs: opts.idleMs } : {}
1577
+ ...opts.idleMs !== void 0 ? { idleMs: opts.idleMs } : {},
1578
+ ...readiness
1417
1579
  });
1418
1580
  made.set(name, r);
1419
1581
  }
@@ -1433,7 +1595,25 @@ import { createServer } from "http";
1433
1595
  import { realpathSync as realpathSync2 } from "fs";
1434
1596
  import { readFile, realpath, stat } from "fs/promises";
1435
1597
  import { extname, resolve, sep } from "path";
1436
- import { parseAst } from "vite";
1598
+ var viteParseAst;
1599
+ var VITE_MISSING = "prerendering needs the vite package, which is not installed: npm i -D vite. To add agent files to HTML you prerendered yourself, use web-for-agents generate, which does not need vite.";
1600
+ function isPackageNotFound(e, pkg) {
1601
+ const code = e?.code;
1602
+ if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") return false;
1603
+ return new RegExp(`(Cannot find (package|module) ['"]${pkg}['"])`).test(errorMessage(e));
1604
+ }
1605
+ async function loadViteParser(importVite = () => import("vite")) {
1606
+ if (viteParseAst) return viteParseAst;
1607
+ let mod;
1608
+ try {
1609
+ mod = await importVite();
1610
+ } catch (e) {
1611
+ if (isPackageNotFound(e, "vite")) throw new Error(withPrefix(VITE_MISSING), { cause: e });
1612
+ throw e;
1613
+ }
1614
+ viteParseAst = mod.parseAst;
1615
+ return viteParseAst;
1616
+ }
1437
1617
  var TYPES = {
1438
1618
  ".html": "text/html; charset=utf-8",
1439
1619
  ".js": "text/javascript; charset=utf-8",
@@ -1458,6 +1638,8 @@ function fixImportMeta(code) {
1458
1638
  }
1459
1639
  function parenthesizeRegExps(code) {
1460
1640
  if (!code.includes("/")) return code;
1641
+ const parseAst = viteParseAst;
1642
+ if (!parseAst) throw new Error(withPrefix("internal: loadViteParser() must run before parenthesizeRegExps"));
1461
1643
  let ast;
1462
1644
  try {
1463
1645
  ast = parseAst(code, { sourceType: "module" });
@@ -1576,7 +1758,8 @@ function startStaticServer(opts) {
1576
1758
  send(500, "text/plain; charset=utf-8", "Internal error");
1577
1759
  }
1578
1760
  });
1579
- return new Promise((resolveP, reject) => {
1761
+ const ready = compat ? loadViteParser() : Promise.resolve();
1762
+ return ready.then(() => new Promise((resolveP, reject) => {
1580
1763
  server.once("error", reject);
1581
1764
  server.listen(0, "127.0.0.1", () => {
1582
1765
  const { port } = server.address();
@@ -1590,7 +1773,7 @@ function startStaticServer(opts) {
1590
1773
  })
1591
1774
  });
1592
1775
  });
1593
- });
1776
+ }));
1594
1777
  }
1595
1778
 
1596
1779
  // src/vite/prerender/index.ts
@@ -1626,6 +1809,8 @@ async function prerenderSite(input) {
1626
1809
  ...pr.executablePath !== void 0 ? { executablePath: pr.executablePath } : {},
1627
1810
  allowOrigins: pr.allowOrigins,
1628
1811
  ...pr.idleMs !== void 0 ? { idleMs: pr.idleMs } : {},
1812
+ ...pr.readySelector !== void 0 ? { readySelector: pr.readySelector } : {},
1813
+ onTimeout: pr.onTimeout,
1629
1814
  codeGeneration: pr.isolation.codeGeneration,
1630
1815
  syncResources
1631
1816
  });
@@ -1647,7 +1832,8 @@ async function prerenderSite(input) {
1647
1832
  contentSelector: markdown ? markdown.contentSelector : DEFAULT_CONTENT_SELECTOR,
1648
1833
  ...markdown ? { markdownUrl: (route) => publicMdUrl(options.siteUrl, route) } : {},
1649
1834
  inPage,
1650
- skipRoute: input.skipRoute
1835
+ skipRoute: input.skipRoute,
1836
+ fileLayout: pr.fileLayout
1651
1837
  },
1652
1838
  async (url, renderer, timeoutMs, scripts) => (await pool.get(renderer)).render(url, timeoutMs, scripts)
1653
1839
  ).catch((e) => {
@@ -1667,10 +1853,10 @@ async function prerenderSite(input) {
1667
1853
  }
1668
1854
  }
1669
1855
  function classicScriptSrcs(html) {
1670
- const { document, dispose } = parseHtml(html);
1856
+ const { document: document2, dispose } = parseHtml(html);
1671
1857
  try {
1672
1858
  const out = [];
1673
- for (const el of Array.from(document.querySelectorAll("script[src]"))) {
1859
+ for (const el of Array.from(document2.querySelectorAll("script[src]"))) {
1674
1860
  if (el.hasAttribute("async") || el.hasAttribute("defer")) continue;
1675
1861
  const type = (el.getAttribute("type") ?? "").trim().toLowerCase();
1676
1862
  if (type !== "" && !/^(text|application)\/(x-)?(java|ecma)script$/.test(type)) continue;
@@ -1768,16 +1954,47 @@ function buildRobots(opts) {
1768
1954
  return blocks.join("\n\n") + "\n";
1769
1955
  }
1770
1956
 
1771
- // src/vite/sitemap.ts
1772
- function escapeXml(s) {
1773
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1957
+ // src/vite/llms.ts
1958
+ var BLOCK_START = /^(\d*)([#>\-+*=_`~.)])/;
1959
+ function paragraphs(text) {
1960
+ if (text === void 0) return [];
1961
+ return text.split(/\n\s*\n/).map((p) => escapeMarkdownInline(p)).filter(Boolean).map((p) => p.replace(BLOCK_START, "$1\\$2"));
1774
1962
  }
1775
- function buildSitemap(siteUrl, routes) {
1776
- const urls = routes.map((r) => ` <url><loc>${escapeXml(new URL(siteUrl + r).href)}</loc></url>`).join("\n");
1777
- return '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' + urls + "\n</urlset>\n";
1963
+ function linkLine(title, url, description) {
1964
+ const desc = escapeMarkdownInline(description);
1965
+ return `- [${escapeMarkdownInline(title)}](${escapeLinkTarget(url)})${desc ? `: ${desc}` : ""}`;
1966
+ }
1967
+ function header(site) {
1968
+ const blocks = [`# ${escapeMarkdownInline(site.title)}`];
1969
+ const desc = escapeMarkdownInline(site.description);
1970
+ if (desc) blocks.push(`> ${desc}`);
1971
+ blocks.push(...paragraphs(site.intro));
1972
+ for (const sec of site.sections ?? []) {
1973
+ const body = [...paragraphs(sec.text)];
1974
+ const links = (sec.links ?? []).map((l) => linkLine(l.title, l.url, l.description));
1975
+ if (links.length) body.push(links.join("\n"));
1976
+ blocks.push([`## ${escapeMarkdownInline(sec.title)}`, ...body].join("\n\n"));
1977
+ }
1978
+ return blocks;
1778
1979
  }
1980
+ function buildLlmsTxt(site, pages, sections = []) {
1981
+ const blocks = header(site);
1982
+ const lines = [...pages].sort(byRoute).map((p) => linkLine(p.title, p.mdUrl ?? p.url, p.description));
1983
+ blocks.push(["## Pages", ...lines.length ? ["", ...lines] : []].join("\n"));
1984
+ for (const s of sections) {
1985
+ blocks.push([`## ${s.heading}`, ...s.lines.length ? ["", ...s.lines] : []].join("\n"));
1986
+ }
1987
+ return blocks.join("\n\n") + "\n";
1988
+ }
1989
+ function buildLlmsFullTxt(site, pages) {
1990
+ const head = header(site).join("\n\n");
1991
+ const blocks = [...pages].sort(byRoute).filter((p) => p.markdown !== void 0).map((p) => `Source: ${p.url}
1779
1992
 
1780
- // src/vite/buildPlugin.ts
1993
+ ${p.markdown.trim()}`);
1994
+ return [head, blocks.join("\n\n---\n\n")].filter(Boolean).join("\n\n") + "\n";
1995
+ }
1996
+
1997
+ // src/vite/siteFiles.ts
1781
1998
  var SPA_FALLBACK_FILE = "200.html";
1782
1999
  var fileOf = (urlPath) => urlPath.slice(1);
1783
2000
  function prefixed(e, context) {
@@ -1792,6 +2009,164 @@ async function callHook(ext, hook, fn) {
1792
2009
  throw prefixed(e, `extension "${ext.name}" ${hook} failed`);
1793
2010
  }
1794
2011
  }
2012
+ function dropFileCollisions(pages, filesOf, taken, warn) {
2013
+ const kept = [];
2014
+ for (const page of pages) {
2015
+ const files = filesOf(page);
2016
+ const clash = files.find((f) => taken.has(f));
2017
+ if (clash !== void 0) {
2018
+ warn(
2019
+ `${LOG_PREFIX} skipped ${shortRoute(page.route)}: its file ${clash} is already the file of ${taken.get(clash)}`
2020
+ );
2021
+ continue;
2022
+ }
2023
+ for (const f of files) taken.set(f, shortRoute(page.route));
2024
+ kept.push(page);
2025
+ }
2026
+ return kept;
2027
+ }
2028
+ async function pageMarkdown(markdown, page) {
2029
+ const md = buildPageMarkdown(page.info.title, page.info.contentHtml, page.url);
2030
+ if (!markdown.transform) return md;
2031
+ let out;
2032
+ try {
2033
+ out = await markdown.transform({
2034
+ route: page.route,
2035
+ url: page.url,
2036
+ title: page.info.title,
2037
+ ...page.info.description !== void 0 ? { description: page.info.description } : {},
2038
+ markdown: md,
2039
+ html: page.html
2040
+ });
2041
+ } catch (e) {
2042
+ throw prefixed(e, `markdown.transform failed for ${shortRoute(page.route)}`);
2043
+ }
2044
+ if (out === void 0) return md;
2045
+ if (typeof out !== "string") {
2046
+ throw new Error(withPrefix(`markdown.transform must return a string or undefined (got ${typeof out} for ${shortRoute(page.route)})`));
2047
+ }
2048
+ return out;
2049
+ }
2050
+ function pageLengthWarnings(listed) {
2051
+ return metaLengthWarnings(
2052
+ listed.map((p) => {
2053
+ const out = { route: p.route };
2054
+ if (p.info.titleFromDocument) out.title = p.info.title;
2055
+ if (p.info.description !== void 0) out.description = p.info.description;
2056
+ return out;
2057
+ })
2058
+ );
2059
+ }
2060
+ async function stageSiteFiles(opts, stager, { pages: allPages, listed, mdByRoute }) {
2061
+ const { siteUrl, markdown, llms, extensions, logger } = opts;
2062
+ for (const ext of extensions) {
2063
+ if (!ext.onRoute) continue;
2064
+ for (const page of listed) {
2065
+ await callHook(ext, `onRoute(${page.route})`, () => ext.onRoute(page.route, page.inPage[ext.name]));
2066
+ }
2067
+ }
2068
+ const rootPage = allPages.find((p) => p.route === "/");
2069
+ const rootTitle = rootPage?.info.titleFromDocument ? rootPage.info.title : void 0;
2070
+ const site = {
2071
+ url: siteUrl,
2072
+ title: (llms ? llms.title : void 0) ?? rootTitle ?? new URL(siteUrl).hostname
2073
+ };
2074
+ const description = (llms ? llms.description : void 0) ?? rootPage?.info.description;
2075
+ if (description !== void 0) site.description = description;
2076
+ const pages = listed.map((p) => {
2077
+ const out = { route: p.route, title: p.info.title, url: publicUrl(siteUrl, p.route) };
2078
+ if (p.info.description !== void 0) out.description = p.info.description;
2079
+ if (markdown) out.mdUrl = publicMdUrl(siteUrl, p.route);
2080
+ return out;
2081
+ });
2082
+ if (llms) {
2083
+ const sections = [];
2084
+ for (const ext of extensions) {
2085
+ if (ext.llmsSections) sections.push(...await callHook(ext, "llmsSections", () => ext.llmsSections()));
2086
+ }
2087
+ const llmsPages = pages.map((p) => {
2088
+ const md = mdByRoute.get(p.route);
2089
+ return md !== void 0 ? { ...p, markdown: md } : { ...p };
2090
+ });
2091
+ const llmsSite = {
2092
+ ...site,
2093
+ ...llms.intro !== void 0 ? { intro: llms.intro } : {},
2094
+ ...llms.sections !== void 0 ? { sections: llms.sections } : {}
2095
+ };
2096
+ stager.stage(fileOf(LLMS_TXT_PATH), buildLlmsTxt(llmsSite, llmsPages, sections));
2097
+ if (markdown) stager.stage(fileOf(LLMS_FULL_TXT_PATH), buildLlmsFullTxt(llmsSite, llmsPages));
2098
+ }
2099
+ for (const ext of extensions) {
2100
+ if (!ext.finalize) continue;
2101
+ const ctx = {
2102
+ site,
2103
+ pages,
2104
+ emitFile: (fileName, source) => {
2105
+ const { file, status } = stager.stage(fileName, source);
2106
+ if (status === "replaced") logger.warn(`${LOG_PREFIX} extension "${ext.name}" overwrote ${file}`);
2107
+ },
2108
+ warn: (msg) => logger.warn(withPrefix(msg))
2109
+ };
2110
+ if (llms) ctx.llmsUrl = publicUrl(siteUrl, LLMS_TXT_PATH);
2111
+ await callHook(ext, "finalize", () => ext.finalize(ctx));
2112
+ }
2113
+ }
2114
+
2115
+ // src/vite/sitemap.ts
2116
+ function escapeXml(s) {
2117
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2118
+ }
2119
+ function buildSitemap(siteUrl, routes, lastmod) {
2120
+ const urls = routes.map((r) => {
2121
+ const date = lastmod?.get(r);
2122
+ const mod = date !== void 0 ? `<lastmod>${escapeXml(date)}</lastmod>` : "";
2123
+ return ` <url><loc>${escapeXml(new URL(siteUrl + r).href)}</loc>${mod}</url>`;
2124
+ }).join("\n");
2125
+ return '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' + urls + "\n</urlset>\n";
2126
+ }
2127
+ function isoDate(d) {
2128
+ return d.toISOString().slice(0, 10);
2129
+ }
2130
+ var DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
2131
+ var DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
2132
+ function validDate(s) {
2133
+ const m = DATE.exec(s.slice(0, 10));
2134
+ if (!m) return false;
2135
+ const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
2136
+ return isoDate(d) === s.slice(0, 10) && (s.length === 10 || !Number.isNaN(Date.parse(s)));
2137
+ }
2138
+ function lastmodValue(v, route) {
2139
+ if (v === void 0) return void 0;
2140
+ if (v instanceof Date && !Number.isNaN(v.getTime())) return isoDate(v);
2141
+ if (typeof v === "string" && (DATE.test(v) || DATETIME.test(v)) && validDate(v)) return v;
2142
+ const got = v instanceof Date ? "an invalid Date" : JSON.stringify(v) ?? typeof v;
2143
+ throw new Error(
2144
+ withPrefix(
2145
+ `sitemap.lastmod must return "YYYY-MM-DD", a W3C datetime such as "2026-01-31T09:00:00Z", a Date or undefined (got ${got} for ${shortRoute(route)})`
2146
+ )
2147
+ );
2148
+ }
2149
+ async function sitemapLastmods(lastmod, routes, fileDate) {
2150
+ const out = /* @__PURE__ */ new Map();
2151
+ if (lastmod === "none") return out;
2152
+ for (const route of routes) {
2153
+ let v;
2154
+ if (lastmod === "file") {
2155
+ v = fileDate(route);
2156
+ } else {
2157
+ try {
2158
+ v = await lastmod(route);
2159
+ } catch (e) {
2160
+ throw prefixed(e, `sitemap.lastmod failed for ${shortRoute(route)}`);
2161
+ }
2162
+ }
2163
+ const value = lastmodValue(v, route);
2164
+ if (value !== void 0) out.set(route, value);
2165
+ }
2166
+ return out;
2167
+ }
2168
+
2169
+ // src/vite/buildPlugin.ts
1795
2170
  function isClientEnv(env) {
1796
2171
  if (!env) return true;
1797
2172
  if (env.config?.consumer) return env.config.consumer === "client";
@@ -1814,6 +2189,7 @@ function createBuildPlugin(resolved, extensions) {
1814
2189
  const shell = fs2.readFileSync(shellFile, "utf8");
1815
2190
  stager.stage(SPA_FALLBACK_FILE, shell);
1816
2191
  const pr = resolved.prerender;
2192
+ const layout = pr === false ? "directory" : pr.fileLayout;
1817
2193
  if (pr !== false && mayUseHappyDom(pr) && !hasPermissionModel() && !warnedPermission) {
1818
2194
  warnedPermission = true;
1819
2195
  logger.warn(NO_PERMISSION_MODEL_WARNING);
@@ -1825,71 +2201,35 @@ function createBuildPlugin(resolved, extensions) {
1825
2201
  base,
1826
2202
  shell,
1827
2203
  options: resolved,
1828
- skipRoute: (route) => stager.inPublicDir(routeHtmlFile(route)),
2204
+ skipRoute: (route) => stager.inPublicDir(routeHtmlFile(route, layout)),
1829
2205
  extensions
1830
2206
  });
1831
2207
  } catch (e) {
1832
2208
  throw prefixed(e, "prerender failed");
1833
2209
  }
2210
+ const filesOf = (route) => [routeHtmlFile(route, layout), ...markdown ? [routeMdFile(route)] : []];
2211
+ const pages = dropFileCollisions(
2212
+ result.pages,
2213
+ (p) => filesOf(p.route),
2214
+ /* @__PURE__ */ new Map([[SPA_FALLBACK_FILE, `the SPA fallback (${SPA_FALLBACK_FILE})`]]),
2215
+ (m) => logger.warn(m)
2216
+ );
2217
+ result = { ...result, pages };
1834
2218
  const mdByRoute = /* @__PURE__ */ new Map();
1835
- for (const page of result.pages) {
1836
- stager.stage(routeHtmlFile(page.route), page.html);
1837
- if (markdown) {
1838
- const md = buildPageMarkdown(page.info.title, page.info.contentHtml, publicUrl(siteUrl, page.route));
1839
- mdByRoute.set(page.route, md);
1840
- stager.stage(routeMdFile(page.route), md);
2219
+ const stageNew = (file, source) => {
2220
+ if (stager.stage(file, source).status === "replaced") {
2221
+ throw new Error(`${LOG_PREFIX} internal error: ${file} was generated twice; nothing was written`);
1841
2222
  }
1842
- }
1843
- return { result, listed: result.pages.filter((p) => p.listed), mdByRoute };
1844
- };
1845
- const runExtensions = async (stager, { result, listed, mdByRoute }) => {
1846
- const { siteUrl, markdown, llms } = resolved;
1847
- for (const ext of extensions) {
1848
- if (!ext.onRoute) continue;
1849
- for (const page of listed) {
1850
- await callHook(ext, `onRoute(${page.route})`, () => ext.onRoute(page.route, page.inPage[ext.name]));
1851
- }
1852
- }
1853
- const rootPage = result.pages.find((p) => p.route === "/");
1854
- const rootTitle = rootPage?.info.titleFromDocument ? rootPage.info.title : void 0;
1855
- const site = {
1856
- url: siteUrl,
1857
- title: (llms ? llms.title : void 0) ?? rootTitle ?? new URL(siteUrl).hostname
1858
2223
  };
1859
- const description = (llms ? llms.description : void 0) ?? rootPage?.info.description;
1860
- if (description !== void 0) site.description = description;
1861
- const pages = listed.map((p) => {
1862
- const out = { route: p.route, title: p.info.title, url: publicUrl(siteUrl, p.route) };
1863
- if (p.info.description !== void 0) out.description = p.info.description;
1864
- if (markdown) out.mdUrl = publicMdUrl(siteUrl, p.route);
1865
- return out;
1866
- });
1867
- if (llms) {
1868
- const sections = [];
1869
- for (const ext of extensions) {
1870
- if (ext.llmsSections) sections.push(...await callHook(ext, "llmsSections", () => ext.llmsSections()));
2224
+ for (const page of pages) {
2225
+ stageNew(routeHtmlFile(page.route, layout), page.html);
2226
+ if (markdown) {
2227
+ const md = await pageMarkdown(markdown, { ...page, url: publicUrl(siteUrl, page.route) });
2228
+ mdByRoute.set(page.route, md);
2229
+ stageNew(routeMdFile(page.route), md);
1871
2230
  }
1872
- const llmsPages = pages.map((p) => {
1873
- const md = mdByRoute.get(p.route);
1874
- return md !== void 0 ? { ...p, markdown: md } : { ...p };
1875
- });
1876
- stager.stage(fileOf(LLMS_TXT_PATH), buildLlmsTxt(site, llmsPages, sections));
1877
- if (markdown) stager.stage(fileOf(LLMS_FULL_TXT_PATH), buildLlmsFullTxt(site, llmsPages));
1878
- }
1879
- for (const ext of extensions) {
1880
- if (!ext.finalize) continue;
1881
- const ctx = {
1882
- site,
1883
- pages,
1884
- emitFile: (fileName, source) => {
1885
- const { file, status } = stager.stage(fileName, source);
1886
- if (status === "replaced") logger.warn(`${LOG_PREFIX} extension "${ext.name}" overwrote ${file}`);
1887
- },
1888
- warn: (msg) => logger.warn(withPrefix(msg))
1889
- };
1890
- if (llms) ctx.llmsUrl = publicUrl(siteUrl, LLMS_TXT_PATH);
1891
- await callHook(ext, "finalize", () => ext.finalize(ctx));
1892
2231
  }
2232
+ return { result, listed: pages.filter((p) => p.listed), mdByRoute };
1893
2233
  };
1894
2234
  const runPipeline = async () => {
1895
2235
  const { siteUrl, markdown } = resolved;
@@ -1899,12 +2239,25 @@ function createBuildPlugin(resolved, extensions) {
1899
2239
  const sitemapRoutes = rendered ? [
1900
2240
  .../* @__PURE__ */ new Set([
1901
2241
  ...rendered.listed.map((p) => p.route),
1902
- ...resolved.routes.filter((r) => stager.inPublicDir(routeHtmlFile(r)))
2242
+ ...resolved.routes.filter(
2243
+ (r) => stager.inPublicDir(routeHtmlFile(r, pr === false ? "directory" : pr.fileLayout))
2244
+ )
1903
2245
  ])
1904
2246
  ].sort() : resolved.routes;
1905
- if (resolved.sitemap) stager.stage(fileOf(SITEMAP_PATH), buildSitemap(siteUrl, sitemapRoutes));
2247
+ if (resolved.sitemap) {
2248
+ const buildDate = /* @__PURE__ */ new Date();
2249
+ const lastmod = await sitemapLastmods(resolved.sitemapLastmod, sitemapRoutes, () => buildDate);
2250
+ stager.stage(fileOf(SITEMAP_PATH), buildSitemap(siteUrl, sitemapRoutes, lastmod));
2251
+ }
1906
2252
  stager.stage(fileOf(ROBOTS_PATH), buildRobots(resolved));
1907
- if (rendered) await runExtensions(stager, rendered);
2253
+ if (rendered) {
2254
+ const { siteUrl: url, markdown: md, llms } = resolved;
2255
+ await stageSiteFiles({ siteUrl: url, markdown: md, llms, extensions, logger }, stager, {
2256
+ pages: rendered.result.pages,
2257
+ listed: rendered.listed,
2258
+ mdByRoute: rendered.mdByRoute
2259
+ });
2260
+ }
1908
2261
  stager.flush();
1909
2262
  if (pr === false || !rendered) return;
1910
2263
  const { result, listed } = rendered;
@@ -1934,6 +2287,7 @@ function createBuildPlugin(resolved, extensions) {
1934
2287
  `${LOG_PREFIX} no [${CONTENT_ATTR}] or "${markdown.contentSelector}" element on ${result.bodyFallbackRoutes.join(", ")}; content was taken from <body> minus header, nav, footer, aside`
1935
2288
  );
1936
2289
  }
2290
+ for (const w of pageLengthWarnings(listed)) logger.warn(w);
1937
2291
  logger.info(
1938
2292
  `${LOG_PREFIX} prerendered ${result.pages.length} routes (${listed.length} listed in llms.txt and the sitemap; the sitemap also keeps configured routes served from publicDir)`
1939
2293
  );
@@ -1979,17 +2333,337 @@ function createBuildPlugin(resolved, extensions) {
1979
2333
  };
1980
2334
  }
1981
2335
 
2336
+ // src/vite/generate.ts
2337
+ import fs3 from "fs";
2338
+ import path4 from "path";
2339
+
2340
+ // src/vite/generateActions.ts
2341
+ function staticActions(document2) {
2342
+ const out = /* @__PURE__ */ new Map();
2343
+ for (const a of markupActions(document2)) {
2344
+ if (a.kind === "link" || !isValidActionName(a.name) || out.has(a.name)) continue;
2345
+ out.set(a.name, {
2346
+ name: a.name,
2347
+ description: a.description?.trim() || a.name,
2348
+ inputSchema: a.kind === "form" ? schemaFromForm(a.element) : { ...EMPTY_OBJECT_SCHEMA, properties: {} },
2349
+ destructive: a.destructive,
2350
+ readOnly: false,
2351
+ kind: a.kind
2352
+ });
2353
+ }
2354
+ return [...out.values()].sort((a, b) => compareStrings(a.name, b.name));
2355
+ }
2356
+
2357
+ // src/vite/generateHtml.ts
2358
+ function detectCharset(bytes) {
2359
+ if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) return "utf-8";
2360
+ if (bytes[0] === 254 && bytes[1] === 255) return "utf-16be";
2361
+ if (bytes[0] === 255 && bytes[1] === 254) return "utf-16le";
2362
+ const head = Buffer.from(bytes.subarray(0, 1024)).toString("latin1");
2363
+ const m = /<meta\b[^>]*?charset\s*=\s*["']?\s*([A-Za-z0-9_:.-]+)/i.exec(head);
2364
+ if (m) {
2365
+ try {
2366
+ const enc = new TextDecoder(m[1]).encoding;
2367
+ return enc.startsWith("utf-16") ? "utf-8" : enc;
2368
+ } catch {
2369
+ }
2370
+ }
2371
+ return "utf-8";
2372
+ }
2373
+ function decodeHtml(bytes) {
2374
+ const charset = detectCharset(bytes);
2375
+ return { charset, text: new TextDecoder(charset).decode(bytes) };
2376
+ }
2377
+ var HEAD_RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "template", "textarea", "title", "noscript"]);
2378
+ var escapeAttr = (href) => href.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/[^\x20-\x7e]/gu, (c) => `&#x${c.codePointAt(0).toString(16)};`);
2379
+ function isMarkdownAlternate(attrs) {
2380
+ const get = (n) => attrs.find((a) => a.name === n)?.value ?? "";
2381
+ const rel = get("rel").toLowerCase().split(/\s+/);
2382
+ return rel.includes("alternate") && get("type").split(";")[0].trim().toLowerCase() === MARKDOWN_MIME;
2383
+ }
2384
+ var ourLink = (attr) => `<link rel="alternate" type="${MARKDOWN_MIME}" href="${attr}" ${HEAD_OWNED_ATTR}="${MARKDOWN_LINK_KEY}">`;
2385
+ function setMarkdownLink(bytes, charset, href) {
2386
+ if (charset.startsWith("utf-16")) return null;
2387
+ const buf = Buffer.from(bytes);
2388
+ const view = buf.toString("latin1");
2389
+ const attr = escapeAttr(href);
2390
+ let headOpenEnd = -1;
2391
+ let headClose = -1;
2392
+ let bodyOpen = -1;
2393
+ let preamble = 0;
2394
+ let marked;
2395
+ for (const t of scanHtml(view, HEAD_RAW_TEXT)) {
2396
+ if (t.type === "comment") {
2397
+ if (headOpenEnd === -1 && /^<!doctype/i.test(view.slice(t.start, t.start + 9))) preamble = t.end;
2398
+ continue;
2399
+ }
2400
+ if (t.type !== "tag") continue;
2401
+ if (t.closing) {
2402
+ if (t.name === "head") {
2403
+ headClose = t.start;
2404
+ break;
2405
+ }
2406
+ continue;
2407
+ }
2408
+ if (t.name === "html" && headOpenEnd === -1) preamble = Math.max(preamble, t.end);
2409
+ if (t.name === "head" && headOpenEnd === -1) headOpenEnd = t.end;
2410
+ else if (t.name === "body") {
2411
+ bodyOpen = t.start;
2412
+ break;
2413
+ } else if (t.name === "link" && isMarkdownAlternate(t.attrs)) {
2414
+ const owner = t.attrs.find((a) => a.name === HEAD_OWNED_ATTR)?.value;
2415
+ if (owner !== MARKDOWN_LINK_KEY) return { bytes: buf, action: "kept" };
2416
+ marked ??= t;
2417
+ }
2418
+ }
2419
+ const splice = (start, end, text, action) => ({
2420
+ bytes: Buffer.concat([buf.subarray(0, start), Buffer.from(text, "latin1"), buf.subarray(end)]),
2421
+ action
2422
+ });
2423
+ if (marked) {
2424
+ const h = marked.attrs.find((a) => a.name === "href");
2425
+ if (h && h.valueStart !== -1) {
2426
+ if (h.value === attr) return { bytes: buf, action: "unchanged" };
2427
+ return splice(h.valueStart, h.valueEnd, h.quote ? attr : `"${attr}"`, "updated");
2428
+ }
2429
+ return splice(marked.start, marked.end, ourLink(attr), "updated");
2430
+ }
2431
+ const at = headClose !== -1 ? headClose : headOpenEnd !== -1 ? headOpenEnd : bodyOpen !== -1 ? bodyOpen : preamble;
2432
+ return splice(at, at, ourLink(attr), "inserted");
2433
+ }
2434
+
2435
+ // src/vite/generate.ts
2436
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["assets", "node_modules"]);
2437
+ var SKIP_FILES = /* @__PURE__ */ new Set([SPA_FALLBACK_FILE, "404.html"]);
2438
+ function htmlFiles(outDir, warn) {
2439
+ const out = [];
2440
+ const walk = (dir, rel) => {
2441
+ for (const e of fs3.readdirSync(dir, { withFileTypes: true })) {
2442
+ if (e.name.startsWith(".")) continue;
2443
+ const r = rel ? `${rel}/${e.name}` : e.name;
2444
+ if (e.isSymbolicLink()) {
2445
+ if (e.name.endsWith(".html") || fs3.statSync(path4.join(dir, e.name), { throwIfNoEntry: false })?.isDirectory()) {
2446
+ warn(`${LOG_PREFIX} ${r} is a symbolic link; skipped`);
2447
+ }
2448
+ } else if (e.isDirectory()) {
2449
+ if (!(rel === "" && SKIP_DIRS.has(e.name))) walk(path4.join(dir, e.name), r);
2450
+ } else if (e.isFile() && e.name.endsWith(".html") && !SKIP_FILES.has(e.name)) {
2451
+ out.push(r);
2452
+ }
2453
+ }
2454
+ };
2455
+ walk(outDir, "");
2456
+ return out.sort(compareStrings);
2457
+ }
2458
+ var GENERATE_OPTION_KEYS = [
2459
+ "outDir",
2460
+ "siteUrl",
2461
+ "robots",
2462
+ "exclude",
2463
+ "sitemap",
2464
+ "markdown",
2465
+ "llms",
2466
+ "agentMap",
2467
+ "linkMarkdown",
2468
+ "overwrite",
2469
+ "logger"
2470
+ ];
2471
+ async function generateAgentFiles(options) {
2472
+ if (!options || typeof options.outDir !== "string" || options.outDir === "") {
2473
+ throw new Error(withPrefix("outDir is required"));
2474
+ }
2475
+ for (const key of ["linkMarkdown", "overwrite"]) {
2476
+ if (options[key] !== void 0 && typeof options[key] !== "boolean") throw new Error(withPrefix(`${key} must be a boolean`));
2477
+ }
2478
+ const resolved = resolveOptions({
2479
+ siteUrl: options.siteUrl,
2480
+ robots: options.robots,
2481
+ ...options.exclude !== void 0 ? { exclude: options.exclude } : {},
2482
+ ...options.sitemap !== void 0 ? { sitemap: options.sitemap } : {},
2483
+ ...options.markdown !== void 0 ? { markdown: options.markdown } : {},
2484
+ ...options.llms !== void 0 ? { llms: options.llms } : {},
2485
+ ...options.agentMap !== void 0 ? { agentMap: options.agentMap } : {},
2486
+ overlay: false
2487
+ });
2488
+ const { siteUrl, markdown } = resolved;
2489
+ const outDir = path4.resolve(options.outDir);
2490
+ if (!fs3.existsSync(outDir) || !fs3.statSync(outDir).isDirectory()) {
2491
+ throw new Error(`${LOG_PREFIX} ${outDir} is not a directory`);
2492
+ }
2493
+ const linkMarkdown = options.linkMarkdown ?? true;
2494
+ const base = options.logger ?? console;
2495
+ const warnings = [];
2496
+ const logger = {
2497
+ warn(msg) {
2498
+ warnings.push(msg);
2499
+ base.warn(msg);
2500
+ }
2501
+ };
2502
+ for (const w of resolved.warnings) logger.warn(w);
2503
+ const unknown = Object.keys(options).filter((k) => !GENERATE_OPTION_KEYS.includes(k));
2504
+ if (unknown.length > 0) {
2505
+ logger.warn(`${LOG_PREFIX} unknown option${unknown.length === 1 ? "" : "s"} ignored: ${unknown.join(", ")}`);
2506
+ }
2507
+ const fileByRoute = /* @__PURE__ */ new Map();
2508
+ for (const file of htmlFiles(outDir, (m) => logger.warn(m))) {
2509
+ const route = routeOfHtmlFile(file);
2510
+ if (!isSafeRoute(route)) {
2511
+ logger.warn(`${LOG_PREFIX} ${file} cannot be a page URL (its name has a backslash or similar); skipped`);
2512
+ continue;
2513
+ }
2514
+ if (route !== "/" && isExcluded(route, resolved.exclude)) continue;
2515
+ const other = fileByRoute.get(route);
2516
+ if (other !== void 0) {
2517
+ logger.warn(`${LOG_PREFIX} ${file} and ${other} are both ${shortRoute(route)}; using ${other}`);
2518
+ continue;
2519
+ }
2520
+ fileByRoute.set(route, file);
2521
+ }
2522
+ if (fileByRoute.size === 0) throw new Error(`${LOG_PREFIX} no .html pages found in ${outDir}`);
2523
+ const files = [...fileByRoute.values()];
2524
+ const flat = files.some((f) => f !== "index.html" && !f.endsWith("/index.html"));
2525
+ const dir = files.some((f) => f.endsWith("/index.html"));
2526
+ const fileLayout = flat && dir ? "mixed" : flat ? "flat" : "directory";
2527
+ const stager = createFileStager({ outDir, publicDir: "", warn: (m) => logger.warn(m) });
2528
+ const written = /* @__PURE__ */ new Set();
2529
+ const kept = [];
2530
+ const protectedFiles = /* @__PURE__ */ new Set([fileOf(ROBOTS_PATH), fileOf(SITEMAP_PATH), fileOf(LLMS_TXT_PATH), fileOf(LLMS_FULL_TXT_PATH)]);
2531
+ if (resolved.agentMap) protectedFiles.add(resolved.agentMap.fileName);
2532
+ const differs = (target, source) => fs3.existsSync(target) && !fs3.readFileSync(target).equals(typeof source === "string" ? Buffer.from(source) : source);
2533
+ const guarded = {
2534
+ ...stager,
2535
+ stage(fileName, source) {
2536
+ const file = fileName.replace(/^\/+/, "");
2537
+ const target = path4.join(outDir, file);
2538
+ if (!options.overwrite && protectedFiles.has(file) && differs(target, source)) {
2539
+ if (!kept.includes(file)) {
2540
+ kept.push(file);
2541
+ logger.warn(`${LOG_PREFIX} ${file} already exists with different content; kept it (use overwrite to replace it)`);
2542
+ }
2543
+ return { file, status: "skipped" };
2544
+ }
2545
+ const r = stager.stage(fileName, source);
2546
+ if (r.status !== "skipped") written.add(r.file);
2547
+ return r;
2548
+ }
2549
+ };
2550
+ const pages = [];
2551
+ const result = [];
2552
+ const mdByRoute = /* @__PURE__ */ new Map();
2553
+ const mtimeByRoute = /* @__PURE__ */ new Map();
2554
+ const bodyFallback = [];
2555
+ const utf16 = [];
2556
+ const ownLinks = [];
2557
+ const routes = dropFileCollisions(
2558
+ [...fileByRoute].map(([route, file]) => ({ route, file })).sort(byRoute),
2559
+ (p) => markdown ? [routeMdFile(p.route)] : [],
2560
+ /* @__PURE__ */ new Map(),
2561
+ (m) => logger.warn(m)
2562
+ );
2563
+ for (const { route, file } of routes) {
2564
+ mtimeByRoute.set(route, fs3.statSync(path4.join(outDir, file)).mtime);
2565
+ const bytes = fs3.readFileSync(path4.join(outDir, file));
2566
+ const { charset, text: html } = decodeHtml(bytes);
2567
+ const url = publicUrl(siteUrl, route);
2568
+ const { document: document2, dispose } = parseHtml(html);
2569
+ let page;
2570
+ try {
2571
+ const actions = resolved.agentMap ? staticActions(document2) : [];
2572
+ const links = resolved.agentMap ? markupLinks(document2) : [];
2573
+ const info = analyzeDocument(document2, {
2574
+ pageUrl: url,
2575
+ contentSelector: markdown ? markdown.contentSelector : "main",
2576
+ route
2577
+ });
2578
+ const inPage = { "agent-map": { actions, links } };
2579
+ page = { route, info, listed: !info.noindex, inPage };
2580
+ } catch (e) {
2581
+ throw prefixed(e, `reading ${file} failed`);
2582
+ } finally {
2583
+ dispose();
2584
+ }
2585
+ pages.push(page);
2586
+ result.push({ route, file, listed: page.listed });
2587
+ if (!markdown) continue;
2588
+ if (page.info.contentSource === "body") bodyFallback.push(route);
2589
+ const md = await pageMarkdown(markdown, { route, url, info: page.info, html });
2590
+ mdByRoute.set(route, md);
2591
+ const mdFile = routeMdFile(route);
2592
+ if (differs(path4.join(outDir, mdFile), md)) {
2593
+ logger.warn(`${LOG_PREFIX} ${mdFile} already exists with different content; replacing it with the markdown of ${shortRoute(route)}`);
2594
+ }
2595
+ guarded.stage(mdFile, md);
2596
+ if (linkMarkdown) {
2597
+ const edit = setMarkdownLink(bytes, charset, publicMdUrl(siteUrl, route));
2598
+ if (edit === null) utf16.push(file);
2599
+ else if (edit.action === "kept") ownLinks.push(shortRoute(route));
2600
+ else if (edit.action !== "unchanged") guarded.stage(file, edit.bytes);
2601
+ }
2602
+ }
2603
+ const listed = pages.filter((p) => p.listed);
2604
+ if (resolved.sitemap) {
2605
+ const routes2 = listed.map((p) => p.route);
2606
+ const lastmod = await sitemapLastmods(resolved.sitemapLastmod, routes2, (r) => mtimeByRoute.get(r));
2607
+ guarded.stage(fileOf(SITEMAP_PATH), buildSitemap(siteUrl, routes2, lastmod));
2608
+ }
2609
+ guarded.stage(fileOf(ROBOTS_PATH), buildRobots(resolved));
2610
+ const extensions = resolved.agentMap ? [agentMapExtension({ siteUrl, fileName: resolved.agentMap.fileName })] : [];
2611
+ await stageSiteFiles({ siteUrl, markdown, llms: resolved.llms, extensions, logger }, guarded, {
2612
+ pages,
2613
+ listed,
2614
+ mdByRoute
2615
+ });
2616
+ let flushed = false;
2617
+ try {
2618
+ stager.flush();
2619
+ flushed = true;
2620
+ } finally {
2621
+ if (!flushed || resolved.sitemapLastmod === "file") {
2622
+ for (const [route, mtime] of mtimeByRoute) {
2623
+ const file = fileByRoute.get(route);
2624
+ if (!written.has(file)) continue;
2625
+ try {
2626
+ fs3.utimesSync(path4.join(outDir, file), mtime, mtime);
2627
+ } catch (e) {
2628
+ if (flushed) throw e;
2629
+ }
2630
+ }
2631
+ }
2632
+ }
2633
+ if (markdown && bodyFallback.length > 0) {
2634
+ logger.warn(
2635
+ `${LOG_PREFIX} no [${CONTENT_ATTR}] or "${markdown.contentSelector}" element on ${bodyFallback.join(", ")}; content was taken from <body> minus header, nav, footer, aside`
2636
+ );
2637
+ }
2638
+ if (ownLinks.length > 0) {
2639
+ base.info(`${LOG_PREFIX} kept the page's own markdown link on ${ownLinks.join(", ")}`);
2640
+ }
2641
+ if (utf16.length > 0) {
2642
+ logger.warn(`${LOG_PREFIX} ${utf16.join(", ")} ${utf16.length === 1 ? "is" : "are"} UTF-16; the markdown link was not added`);
2643
+ }
2644
+ for (const w of pageLengthWarnings(listed)) logger.warn(w);
2645
+ base.info(`${LOG_PREFIX} generated agent files for ${pages.length} pages (${listed.length} listed) in ${outDir}`);
2646
+ return {
2647
+ outDir,
2648
+ fileLayout,
2649
+ pages: result.sort(byRoute),
2650
+ written: [...written].sort(compareStrings),
2651
+ kept: kept.sort(compareStrings),
2652
+ warnings
2653
+ };
2654
+ }
2655
+
1982
2656
  // src/vite/overlay.ts
1983
2657
  import { existsSync as existsSync2 } from "fs";
1984
2658
  import { createRequire as createRequire2 } from "module";
1985
- import path4 from "path";
2659
+ import path5 from "path";
1986
2660
  import { fileURLToPath as fileURLToPath2 } from "url";
1987
2661
  var OVERLAY_VIRTUAL_ID = "virtual:web-for-agents/overlay";
1988
2662
  var RESOLVED_ID = "\0" + OVERLAY_VIRTUAL_ID;
1989
2663
  var PACKAGE_NAME = "@anubhav-choubey/web-for-agents";
1990
2664
  var OVERLAY_DEPS = ["turndown", "turndown-plugin-gfm"];
1991
2665
  function overlayDepsFor(root) {
1992
- const req = createRequire2(path4.join(root, "noop.js"));
2666
+ const req = createRequire2(path5.join(root, "noop.js"));
1993
2667
  return OVERLAY_DEPS.map((dep) => {
1994
2668
  try {
1995
2669
  req.resolve(dep);
@@ -2014,7 +2688,7 @@ function overlayPlugin(o, siteUrl) {
2014
2688
  name: "web-for-agents:overlay",
2015
2689
  apply: (_config, env) => env.command === "serve" && !process.env.VITEST,
2016
2690
  config(config) {
2017
- return { optimizeDeps: { include: overlayDepsFor(path4.resolve(config.root ?? process.cwd())) } };
2691
+ return { optimizeDeps: { include: overlayDepsFor(path5.resolve(config.root ?? process.cwd())) } };
2018
2692
  },
2019
2693
  configResolved(config) {
2020
2694
  base = config.base;
@@ -2045,5 +2719,6 @@ function webForAgents(options) {
2045
2719
  }
2046
2720
  export {
2047
2721
  AI_BOTS,
2722
+ generateAgentFiles,
2048
2723
  webForAgents
2049
2724
  };