nuxt-ai-ready 3.2.0 → 3.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.
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { realpath, readFile, readdir } from 'node:fs/promises';
3
3
  import { join, resolve, isAbsolute, relative, sep } from 'node:path';
4
4
  import { parseDocument } from 'yaml';
5
- import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.DH9mSayy.mjs';
5
+ import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.BwnSMqRn.mjs';
6
6
  import 'node:module';
7
7
  import 'node:url';
8
8
  import '@nuxt/kit';
@@ -11,6 +11,7 @@ import 'nuxt-site-config/kit';
11
11
  import 'nuxtseo-shared/kit';
12
12
  import '../shared/nuxt-ai-ready.B3hAuOLQ.mjs';
13
13
  import 'node:fs';
14
+ import '../../dist/runtime/llms-txt-files.js';
14
15
  import '../../dist/runtime/route-path.js';
15
16
  import '../../dist/runtime/server/utils/sitemap-md.js';
16
17
  import '../../dist/runtime/cache-control.js';
@@ -1,16 +1,16 @@
1
- import { mkdir, writeFile, appendFile, stat, readdir } from 'node:fs/promises';
2
- import { join, dirname, relative, resolve } from 'node:path';
1
+ import { mkdir, writeFile, appendFile, readFile, stat, readdir } from 'node:fs/promises';
2
+ import { resolve, relative, join, dirname } from 'node:path';
3
3
  import { useNuxt, getNitroVersion, hasNuxtModule, resolveFiles } from '@nuxt/kit';
4
4
  import { colorize } from 'consola/utils';
5
5
  import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
6
6
  import { createPrerenderFetch } from 'nuxtseo-shared/prerender';
7
7
  import { collectSitemap } from 'sitemapd/parse';
8
- import { withLeadingSlash, joinURL, withBase } from 'ufo';
9
- import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.DH9mSayy.mjs';
8
+ import { parseURL, withLeadingSlash, joinURL, withBase } from 'ufo';
9
+ import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.BwnSMqRn.mjs';
10
+ import { toLogicalRoute, toDeployedRoute } from '../../dist/runtime/route-path.js';
10
11
  import { insertLlmsTxtDirective } from '../../dist/runtime/llms-txt-directive.js';
11
12
  import { absoluteSiteUrl } from '../../dist/runtime/markdown-links.js';
12
- import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
13
- import { toDeployedRoute, toLogicalRoute } from '../../dist/runtime/route-path.js';
13
+ import { normalizePagePath, hasMarkdownTwin, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
14
14
  import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
15
15
  import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../../dist/runtime/server/utils/llms-full.js';
16
16
  import { resolvePageUpdatedAt } from '../../dist/runtime/server/utils/page-updated-at.js';
@@ -23,6 +23,7 @@ import 'nuxt-site-config/kit';
23
23
  import 'nuxtseo-shared/kit';
24
24
  import '../shared/nuxt-ai-ready.B3hAuOLQ.mjs';
25
25
  import 'node:fs';
26
+ import '../../dist/runtime/llms-txt-files.js';
26
27
  import '../../dist/runtime/cache-control.js';
27
28
  import '../../dist/runtime/server/utils/discovery-response.js';
28
29
  import 'confbox';
@@ -31,6 +32,33 @@ import '../../dist/runtime/server/utils/content-negotiation.js';
31
32
  import 'pathe';
32
33
  import 'nuxtseo-shared/i18n';
33
34
 
35
+ function parsePrerenderRoute(input, baseURL) {
36
+ let pathname = parseURL(input).pathname;
37
+ try {
38
+ pathname = decodeURIComponent(pathname);
39
+ } catch {
40
+ return { _tag: "Invalid" };
41
+ }
42
+ while (true) {
43
+ if (pathname.includes("\\") || [...pathname].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) || pathname.split("/").some((segment) => segment === "." || segment === ".."))
44
+ return { _tag: "Invalid" };
45
+ const decoded = pathname.replace(/%([\da-f]{2})/gi, (_, byte) => String.fromCharCode(Number.parseInt(byte, 16)));
46
+ if (decoded === pathname)
47
+ break;
48
+ pathname = decoded;
49
+ }
50
+ return { _tag: "Valid", route: toLogicalRoute(input, baseURL) };
51
+ }
52
+ function resolvePrerenderFile(root, route) {
53
+ if (parsePrerenderRoute(route, "/")._tag === "Invalid")
54
+ throw new Error(`Unsafe prerender file path: ${route}`);
55
+ const destination = resolve(root, `.${route.startsWith("/") ? route : `/${route}`}`);
56
+ const child = relative(resolve(root), destination);
57
+ if (child === ".." || child.startsWith("../") || child.startsWith("..\\"))
58
+ throw new Error(`Prerender file path escapes its root: ${route}`);
59
+ return destination;
60
+ }
61
+
34
62
  const PRERENDER_PAGE_TIMEOUT = 3e4;
35
63
  const RE_HTML_MD_EXT = /\.(html|md)$/;
36
64
  const RE_INDEX_SUFFIX = /\/index$/;
@@ -186,7 +214,32 @@ async function processMarkdownRoute(state, nuxt, route, parsed, lastmod, options
186
214
  async function processSitemapEntry(state, nuxt, nitro, entry) {
187
215
  const loc = typeof entry === "string" ? entry : entry.loc;
188
216
  const lastmod = typeof entry === "string" ? void 0 : entry.lastmod;
189
- const route = toLogicalRoute(loc, nitro.options.baseURL);
217
+ const parsedRoute = parsePrerenderRoute(loc, nitro.options.baseURL);
218
+ if (parsedRoute._tag === "Invalid") {
219
+ logger.warn(`Skipping unsafe sitemap URL: ${loc}`);
220
+ return { crawled: false, skipped: true };
221
+ }
222
+ const route = parsedRoute.route;
223
+ if (!hasMarkdownTwin(route))
224
+ return { crawled: false, skipped: true };
225
+ if (nitro.options.static) {
226
+ for (const asset of nitro.options.publicAssets) {
227
+ const assetBase = asset.baseURL || "/";
228
+ if (!route.startsWith(assetBase))
229
+ continue;
230
+ const assetRoute = route.slice(assetBase.length);
231
+ for (const htmlRoute of [`${assetRoute}.html`, `${assetRoute}/index.html`]) {
232
+ const htmlPath = resolvePrerenderFile(asset.dir, htmlRoute);
233
+ const existing = await stat(htmlPath).catch((error) => {
234
+ if (error.code === "ENOENT")
235
+ return void 0;
236
+ throw error;
237
+ });
238
+ if (existing?.isFile())
239
+ return { crawled: false, skipped: true };
240
+ }
241
+ }
242
+ }
190
243
  if (route.split("/").some((segment) => segment.startsWith("_"))) {
191
244
  return { crawled: false, skipped: true };
192
245
  }
@@ -195,12 +248,24 @@ async function processSitemapEntry(state, nuxt, nitro, entry) {
195
248
  }
196
249
  const mdRoute = toMarkdownPath(route);
197
250
  const mdUrl = toDeployedRoute(mdRoute, nitro.options.baseURL);
251
+ const candidates = [route, toDeployedRoute(route, nitro.options.baseURL), mdRoute, mdUrl];
252
+ const ignored = nitro.options.prerender.ignore.some((pattern) => candidates.some((path) => {
253
+ if (typeof pattern === "string")
254
+ return path.startsWith(pattern);
255
+ if (typeof pattern === "function")
256
+ return pattern(path) === true;
257
+ return pattern.test(path);
258
+ }));
259
+ if (ignored)
260
+ return { crawled: false, skipped: true };
198
261
  logger.debug(`Fetching markdown for ${route} \u2192 ${mdUrl}`);
199
262
  const res = await crawlerFetch(state)(mdUrl, {
200
263
  headers: { "x-nitro-prerender": mdRoute },
201
264
  retry: 0,
202
265
  signal: AbortSignal.timeout(PRERENDER_PAGE_TIMEOUT)
203
266
  }).catch((err) => {
267
+ if (err?.response?.status === 404 || err?.response?.status === 410)
268
+ state.errorRoutes.add(route);
204
269
  if (err?.name === "TimeoutError" || err?.name === "AbortError")
205
270
  logger.warn(`Timeout (${PRERENDER_PAGE_TIMEOUT}ms) fetching markdown for ${route}`);
206
271
  else
@@ -217,6 +282,29 @@ async function processSitemapEntry(state, nuxt, nitro, entry) {
217
282
  return { crawled: false, skipped: false };
218
283
  }
219
284
  await processMarkdownRoute(state, nuxt, route, parsed, lastmod, { skipLlmsFullTxt: true });
285
+ if (nitro.options.static) {
286
+ for (const asset of nitro.options.publicAssets) {
287
+ const assetBase = asset.baseURL || "/";
288
+ if (!mdRoute.startsWith(assetBase))
289
+ continue;
290
+ const source = resolvePrerenderFile(asset.dir, mdRoute.slice(assetBase.length));
291
+ const existing = await stat(source).catch((error) => {
292
+ if (error.code === "ENOENT")
293
+ return void 0;
294
+ throw error;
295
+ });
296
+ if (existing?.isFile())
297
+ return { crawled: true, skipped: false };
298
+ }
299
+ const generated = { route: mdRoute, fileName: mdRoute, contents: JSON.stringify(parsed) };
300
+ await nitro.hooks.callHook("prerender:generate", generated, nitro);
301
+ if (generated.skip || generated.error)
302
+ return { crawled: true, skipped: false };
303
+ const filePath = resolvePrerenderFile(nitro.options.output.publicDir, mdRoute);
304
+ await mkdir(dirname(filePath), { recursive: true });
305
+ await writeFile(filePath, generated.contents || "", "utf8");
306
+ nitro._prerenderedRoutes.push(generated);
307
+ }
220
308
  return { crawled: true, skipped: false };
221
309
  }
222
310
  async function crawlSitemapEntries(state, nuxt, nitro, entries) {
@@ -237,18 +325,72 @@ async function crawlSitemapEntries(state, nuxt, nitro, entries) {
237
325
  logger.debug(`Sitemap crawl complete: ${crawled} crawled, ${skipped} skipped`);
238
326
  return crawled;
239
327
  }
240
- async function crawlSitemapContent(state, nuxt, nitro, sitemapContent) {
328
+ async function crawlSitemapContent(state, nuxt, nitro, sitemapContent, seen = /* @__PURE__ */ new Set(), depth = 0, documents = /* @__PURE__ */ new Map()) {
241
329
  logger.debug(`Parsing sitemap XML (${sitemapContent.length} bytes)`);
242
330
  const result = await collectSitemap(sitemapContent);
243
- if (result._tag !== "document" || result.document._tag !== "urlset") {
331
+ if (result._tag !== "document") {
244
332
  const issues = result.issues.map((issue) => issue.message).join("; ");
245
333
  logger.debug(`Skipping sitemap: ${issues || "document is not a URL set"}`);
246
334
  return 0;
247
335
  }
336
+ if (result.document._tag === "index") {
337
+ if (depth >= 3 || seen.size >= 100) {
338
+ logger.warn("Sitemap crawl reached its document limit.");
339
+ return 0;
340
+ }
341
+ let crawled = 0;
342
+ const origin = new URL(state.siteInfo?.url || "http://localhost").origin;
343
+ for (const entry of result.document.entries) {
344
+ if (parsePrerenderRoute(entry.loc, nitro.options.baseURL)._tag === "Invalid")
345
+ continue;
346
+ const url = new URL(entry.loc, origin);
347
+ if (url.origin !== origin || seen.has(url.href))
348
+ continue;
349
+ if (seen.size >= 100) {
350
+ logger.warn("Sitemap crawl reached its document limit.");
351
+ break;
352
+ }
353
+ seen.add(url.href);
354
+ const content = documents.get(toLogicalRoute(url.pathname, nitro.options.baseURL))?.() ?? await crawlerFetch(state)(`${url.pathname}${url.search}`, {
355
+ headers: { "x-nitro-prerender": url.pathname },
356
+ retry: 0,
357
+ signal: AbortSignal.timeout(PRERENDER_PAGE_TIMEOUT)
358
+ }).catch((error) => {
359
+ logger.warn(`Sitemap crawl failed for ${url.pathname}: ${String(error)}`);
360
+ return null;
361
+ });
362
+ if (content)
363
+ crawled += await crawlSitemapContent(state, nuxt, nitro, content, seen, depth + 1, documents);
364
+ }
365
+ return crawled;
366
+ }
248
367
  const urls = result.document.entries;
249
368
  logger.debug(`Found ${urls.length} URLs in sitemap`);
250
369
  return crawlSitemapEntries(state, nuxt, nitro, urls);
251
370
  }
371
+ async function crawlConfiguredSitemaps(state, nuxt, nitro, sitemap, documents = /* @__PURE__ */ new Map()) {
372
+ const names = state.llmsTxtConfig?.sitemaps;
373
+ const singleName = (sitemap.sitemapName || "sitemap.xml").replace(/\.xml$/, "");
374
+ const roots = sitemap.sitemaps && typeof sitemap.sitemaps === "object" ? Object.entries(sitemap.sitemaps).flatMap(([key, definition]) => {
375
+ const name = (definition.sitemapName || key).replace(/\.xml$/, "");
376
+ if (key === "index" || names !== void 0 && !names.includes(name))
377
+ return [];
378
+ const route = definition._route || definition.route;
379
+ return [route ? toLogicalRoute(route, nitro.options.baseURL) : joinURL(sitemap.sitemapsPathPrefix ?? "/__sitemap__/", `${name}.xml`)];
380
+ }) : names !== void 0 && !names.includes(sitemap.sitemaps ? "chunks" : singleName) ? [] : [sitemap.sitemaps ? "/sitemap_index.xml" : `/${sitemap.sitemapName || "sitemap.xml"}`];
381
+ for (const root of roots) {
382
+ const content = documents.get(root)?.() ?? await crawlerFetch(state)(toDeployedRoute(root, nitro.options.baseURL), {
383
+ headers: { "x-nitro-prerender": root },
384
+ retry: 0,
385
+ signal: AbortSignal.timeout(PRERENDER_PAGE_TIMEOUT)
386
+ }).catch((error) => {
387
+ logger.warn(`Sitemap crawl failed for ${root}: ${String(error)}`);
388
+ return null;
389
+ });
390
+ if (content)
391
+ await crawlSitemapContent(state, nuxt, nitro, content, /* @__PURE__ */ new Set(), 0, documents);
392
+ }
393
+ }
252
394
  function isNuxtGenerate() {
253
395
  return process.argv.includes("generate") || process.env.NUXT_GENERATE === "true" || process.env.prerender === "true";
254
396
  }
@@ -274,6 +416,7 @@ function detectSitemapPrerender(sitemapName = "sitemap.xml") {
274
416
  };
275
417
  }
276
418
  async function prerenderRoute(nitro, route, fetch) {
419
+ const filePath = resolvePrerenderFile(nitro.options.output.publicDir, route);
277
420
  const start = Date.now();
278
421
  const encodedRoute = encodeURI(route);
279
422
  const fetchUrl = withBase(encodedRoute, nitro.options.baseURL);
@@ -282,7 +425,6 @@ async function prerenderRoute(nitro, route, fetch) {
282
425
  retry: nitro.options.prerender.retry,
283
426
  retryDelay: nitro.options.prerender.retryDelay
284
427
  });
285
- const filePath = join(nitro.options.output.publicDir, route);
286
428
  logger.debug(`Creating directory for prerender: ${dirname(filePath)}`);
287
429
  await mkdir(dirname(filePath), { recursive: true });
288
430
  const data = res._data;
@@ -304,7 +446,7 @@ async function prerenderRoute(nitro, route, fetch) {
304
446
  function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras) {
305
447
  const nuxt = useNuxt();
306
448
  nuxt.hooks.hook("nitro:init", async (nitro) => {
307
- const llmsFullTxtPath = join(nitro.options.output.publicDir, "llms-full.txt");
449
+ const llmsFullTxtPath = options.llmsFullTxt === false ? void 0 : join(nitro.options.output.publicDir, "llms-full.txt");
308
450
  const state = createCrawlerState(
309
451
  dbPath,
310
452
  llmsFullTxtPath,
@@ -348,7 +490,8 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
348
490
  initPromise = initCrawler(state);
349
491
  await initPromise;
350
492
  const parsed = JSON.parse(route.contents || "{}");
351
- await processMarkdownRoute(state, nuxt, pageRoute, parsed);
493
+ if (!state.prerenderedRoutes.has(pageRoute))
494
+ await processMarkdownRoute(state, nuxt, pageRoute, parsed);
352
495
  const withSitemap = options.sitemapMd === false ? parsed.markdown : appendSitemapSection(parsed.markdown, absoluteSiteUrl(toDeployedRoute(SITEMAP_MD_ROUTE, nitro.options.baseURL), siteInfo?.url));
353
496
  route.contents = llmsTxtConfig?.directive?.markdown === false ? withSitemap : insertLlmsTxtDirective(withSitemap, absoluteSiteUrl(toDeployedRoute("/llms.txt", nitro.options.baseURL), siteInfo?.url));
354
497
  route.contentType = "text/markdown; charset=utf-8";
@@ -426,20 +569,46 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
426
569
  paths
427
570
  }), "utf-8");
428
571
  }
429
- const llmsStats = await prerenderRoute(nitro, "/llms.txt", crawlerFetch(state));
430
- const llmsFullStats = await stat(state.llmsFullTxtPath);
431
- nitro._prerenderedRoutes.push({ route: "/llms-full.txt", fileName: "/llms-full.txt" });
572
+ let llmsSize = 0;
573
+ if (options.llmsTxt !== false) {
574
+ const seen = /* @__PURE__ */ new Set();
575
+ const pending = ["/llms.txt"];
576
+ while (pending.length) {
577
+ const route = pending.shift();
578
+ if (seen.has(route))
579
+ continue;
580
+ seen.add(route);
581
+ const stats = await prerenderRoute(nitro, route, crawlerFetch(state));
582
+ llmsSize += stats.size;
583
+ const body = await readFile(resolvePrerenderFile(nitro.options.output.publicDir, route), "utf-8");
584
+ for (const match of body.matchAll(/\]\(([^)]+)\)/g)) {
585
+ const origin = new URL(siteInfo?.url || "http://localhost").origin;
586
+ const parsedTarget = parsePrerenderRoute(match[1], nitro.options.baseURL);
587
+ if (parsedTarget._tag === "Invalid")
588
+ continue;
589
+ const target = new URL(match[1], origin);
590
+ if (target.origin !== origin)
591
+ continue;
592
+ const path = parsedTarget.route;
593
+ if (path.startsWith("/llms/") && path.endsWith(".txt"))
594
+ pending.push(path);
595
+ }
596
+ }
597
+ }
598
+ const llmsFullStats = state.llmsFullTxtPath ? await stat(state.llmsFullTxtPath) : { size: 0 };
599
+ if (state.llmsFullTxtPath)
600
+ nitro._prerenderedRoutes.push({ route: "/llms-full.txt", fileName: "/llms-full.txt" });
432
601
  if (options.sitemapMd !== false) {
433
602
  const sitemapMdStats = await prerenderRoute(nitro, SITEMAP_MD_ROUTE, crawlerFetch(state));
434
603
  logger.debug(`Wrote sitemap.md (${(sitemapMdStats.size / 1024).toFixed(1)}kb)`);
435
604
  }
436
605
  const kb = (b) => (b / 1024).toFixed(1);
437
- const totalKb = kb(llmsStats.size + llmsFullStats.size);
606
+ const totalKb = kb(llmsSize + llmsFullStats.size);
438
607
  const dim = (s) => colorize("dim", s);
439
608
  const cyan = (s) => colorize("cyan", s);
440
609
  const timeStr = state.totalProcessingTime >= 100 ? ` in ${cyan(`${(state.totalProcessingTime / 1e3).toFixed(1)}s`)}` : "";
441
610
  logger.info(`Indexed ${cyan(String(state.prerenderedRoutes.size))} pages for llms.txt${timeStr} \u2192 ${cyan(`${totalKb}kb`)}`);
442
- logger.info(dim(` llms.txt: ${kb(llmsStats.size)}kb, llms-full.txt: ${kb(llmsFullStats.size)}kb`));
611
+ logger.info(dim(` llms.txt files: ${kb(llmsSize)}kb, llms-full.txt: ${kb(llmsFullStats.size)}kb`));
443
612
  }
444
613
  const { useSitemapHook, usePrerenderHook } = detectSitemapPrerender();
445
614
  logger.debug(`Prerender hooks: sitemap=${useSitemapHook}, prerender=${usePrerenderHook}`);
@@ -447,8 +616,8 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
447
616
  nuxt.hooks.hook("sitemap:prerender:done", async (ctx) => (async () => {
448
617
  if (!state.initialized)
449
618
  return;
450
- for (const sitemap of ctx.sitemaps)
451
- await crawlSitemapContent(state, nuxt, nitro, sitemap.content);
619
+ const documents = new Map(ctx.sitemaps.map((sitemap) => [toLogicalRoute(sitemap.name, nitro.options.baseURL), () => sitemap.content]));
620
+ await crawlConfiguredSitemaps(state, nuxt, nitro, ctx.options, documents);
452
621
  await writeLlmsFiles();
453
622
  state.prerenderedRoutes.clear();
454
623
  if (state.db)
@@ -458,14 +627,7 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
458
627
  nitro.hooks.hook("prerender:done", async () => (async () => {
459
628
  if (!state.initialized)
460
629
  return;
461
- const sitemapContent = await crawlerFetch(state)(toDeployedRoute("/sitemap.xml", nitro.options.baseURL), {
462
- headers: { "x-nitro-prerender": "/sitemap.xml" },
463
- signal: AbortSignal.timeout(PRERENDER_PAGE_TIMEOUT)
464
- }).catch(() => {
465
- return null;
466
- });
467
- if (sitemapContent)
468
- await crawlSitemapContent(state, nuxt, nitro, sitemapContent);
630
+ await crawlConfiguredSitemaps(state, nuxt, nitro, nuxt.options.sitemap || {});
469
631
  await writeLlmsFiles();
470
632
  state.prerenderedRoutes.clear();
471
633
  if (state.db)
package/dist/module.d.mts CHANGED
@@ -101,6 +101,7 @@ interface ModulePublicRuntimeConfig {
101
101
  version: string;
102
102
  sitemapMd: boolean;
103
103
  describedby: boolean;
104
+ llmsFullTxt?: boolean;
104
105
  mdreamOptions: ModuleOptions['mdreamOptions'];
105
106
  markdownCacheHeaders: Required<NonNullable<ModuleOptions['markdownCacheHeaders']>>;
106
107
  database: ResolvedDatabase;
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": "^4.6.0 || ^5.0.0-0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "3.2.0",
7
+ "version": "3.3.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,8 +7,9 @@ import '@nuxt/kit';
7
7
  import 'defu';
8
8
  import 'nuxt-site-config/kit';
9
9
  import 'nuxtseo-shared/kit';
10
- export { m as default } from './shared/nuxt-ai-ready.DH9mSayy.mjs';
10
+ export { m as default } from './shared/nuxt-ai-ready.BwnSMqRn.mjs';
11
11
  import './shared/nuxt-ai-ready.B3hAuOLQ.mjs';
12
+ import '../dist/runtime/llms-txt-files.js';
12
13
  import '../dist/runtime/route-path.js';
13
14
  import '../dist/runtime/server/utils/sitemap-md.js';
14
15
  import '../dist/runtime/server/utils/discovery-response.js';
@@ -0,0 +1,30 @@
1
+ import type { LlmsTxtConfig, LlmsTxtSection } from './types.js';
2
+ export interface LlmsTxtPage {
3
+ pathname: string;
4
+ href?: string;
5
+ title?: string;
6
+ description?: string;
7
+ locale?: string;
8
+ }
9
+ export declare function llmsTxtSectionPath(section: LlmsTxtSection): string;
10
+ export type ParsedLlmsTxtConfig = {
11
+ _tag: 'Valid';
12
+ config: LlmsTxtConfig;
13
+ } | {
14
+ _tag: 'Invalid';
15
+ message: string;
16
+ };
17
+ /** Parse the public configuration once, before Nitro registers routes. */
18
+ export declare function parseLlmsTxtConfig(input: unknown): ParsedLlmsTxtConfig;
19
+ interface LlmsTxtFilesInput {
20
+ rootContent?: string;
21
+ header: string;
22
+ config: LlmsTxtConfig;
23
+ sections: LlmsTxtSection[];
24
+ pages: LlmsTxtPage[];
25
+ sitemapPages: ReadonlyMap<string, LlmsTxtPage[]>;
26
+ resolveUrl: (path: string) => string;
27
+ }
28
+ /** A bounded tree of file lists. Every page appears once; every file is reachable from the root. */
29
+ export declare function createLlmsTxtFiles(input: LlmsTxtFilesInput): Map<string, string>;
30
+ export {};
@@ -0,0 +1,141 @@
1
+ import { formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
2
+ const FILE_PATH = /^\/llms\/[a-z0-9][\w-]*\.txt$/i;
3
+ const SITEMAP_NAME = /^[a-z0-9][\w-]*$/i;
4
+ export function llmsTxtSectionPath(section) {
5
+ const name = Array.isArray(section.sitemap) ? section.sitemap[0] : section.sitemap;
6
+ return section.path || `/llms/${name}.txt`;
7
+ }
8
+ export function parseLlmsTxtConfig(input) {
9
+ const invalid = (message) => ({ _tag: "Invalid", message });
10
+ if (!input || typeof input !== "object" || Array.isArray(input))
11
+ return invalid("llmsTxt must be an object or false.");
12
+ const config = input;
13
+ if (config.pages !== void 0 && config.pages !== false && config.pages !== "sitemap" && config.pages !== "all")
14
+ return invalid("llmsTxt.pages must be sitemap, all, or false.");
15
+ if (config.maxLength !== void 0 && (!Number.isSafeInteger(config.maxLength) || config.maxLength < 1024))
16
+ return invalid("llmsTxt.maxLength must be an integer of at least 1024.");
17
+ if (config.sitemaps !== void 0 && (!Array.isArray(config.sitemaps) || config.sitemaps.some((name) => typeof name !== "string" || !SITEMAP_NAME.test(name))))
18
+ return invalid("llmsTxt.sitemaps must contain sitemap names.");
19
+ if (config.sections !== void 0 && !Array.isArray(config.sections))
20
+ return invalid("llmsTxt.sections must be an array.");
21
+ const paths = /* @__PURE__ */ new Set();
22
+ for (const section of config.sections || []) {
23
+ if (!section || typeof section !== "object" || typeof section.title !== "string")
24
+ return invalid("Each llmsTxt section needs a title.");
25
+ if (section.sitemap === void 0) {
26
+ if (section.path !== void 0)
27
+ return invalid("A section path requires a sitemap.");
28
+ continue;
29
+ }
30
+ const names = Array.isArray(section.sitemap) ? section.sitemap : [section.sitemap];
31
+ if (!names.length || names.some((name) => typeof name !== "string" || !SITEMAP_NAME.test(name)))
32
+ return invalid("Each section sitemap must contain a sitemap name.");
33
+ const path = llmsTxtSectionPath(section);
34
+ if (!FILE_PATH.test(path))
35
+ return invalid("A section path must use /llms/<name>.txt.");
36
+ if (paths.has(path))
37
+ return invalid(`Duplicate llmsTxt section path: ${path}.`);
38
+ paths.add(path);
39
+ }
40
+ return { _tag: "Valid", config };
41
+ }
42
+ export function createLlmsTxtFiles(input) {
43
+ const files = /* @__PURE__ */ new Map();
44
+ const maxLength = input.config.maxLength ?? 8e4;
45
+ const link = (title, path, description) => formatLlmsTxtPageLink({ pathname: path, href: input.resolveUrl(path), title, description });
46
+ const render = (header, title, lines) => `${header.trim()}
47
+
48
+ ## ${title.replace(/\s+/g, " ").trim()}
49
+
50
+ ${lines.join("\n")}
51
+ `;
52
+ function tree(path, prefix, header, title, lines, level = 0) {
53
+ const body = render(header, title, lines);
54
+ if (body.length <= maxLength) {
55
+ files.set(path, body);
56
+ return;
57
+ }
58
+ const overhead = render(header, title, []).length;
59
+ const chunks = [];
60
+ let chunk = [];
61
+ let length = overhead;
62
+ for (const line of lines) {
63
+ if (overhead + line.length + 1 > maxLength)
64
+ throw new Error("An llms.txt entry exceeds llmsTxt.maxLength. Shorten the entry or increase the limit.");
65
+ if (length + line.length + 1 > maxLength) {
66
+ chunks.push(chunk);
67
+ chunk = [];
68
+ length = overhead;
69
+ }
70
+ chunk.push(line);
71
+ length += line.length + 1;
72
+ }
73
+ if (chunk.length)
74
+ chunks.push(chunk);
75
+ if (chunks.length >= lines.length)
76
+ throw new Error("llmsTxt.maxLength cannot fit an index. Increase the limit.");
77
+ const links = chunks.map((entries, index) => {
78
+ const child = `/llms/_${prefix}-${level}-${index + 1}.txt`;
79
+ files.set(child, render(header, title, entries));
80
+ return link(`${title} ${index + 1}`, child);
81
+ });
82
+ tree(path, prefix, header, title, links, level + 1);
83
+ }
84
+ const sections = [];
85
+ const assigned = /* @__PURE__ */ new Set();
86
+ for (const [index, section] of input.sections.entries()) {
87
+ if (!section.sitemap) {
88
+ sections.push(section);
89
+ continue;
90
+ }
91
+ const names = Array.isArray(section.sitemap) ? section.sitemap : [section.sitemap];
92
+ const pages2 = /* @__PURE__ */ new Map();
93
+ for (const name of names) {
94
+ for (const page of input.sitemapPages.get(name) || []) {
95
+ if (!assigned.has(page.pathname))
96
+ pages2.set(page.pathname, page);
97
+ }
98
+ }
99
+ const entries = [...(section.links || []).map((entry) => formatLlmsTxtPageLink({ pathname: entry.href, ...entry })), ...[...pages2.values()].map(formatLlmsTxtPageLink)];
100
+ if (!entries.length)
101
+ continue;
102
+ for (const pathname of pages2.keys())
103
+ assigned.add(pathname);
104
+ const path = llmsTxtSectionPath(section);
105
+ const description = Array.isArray(section.description) ? section.description.join(" ") : section.description;
106
+ const preamble = normalizeLlmsTxtConfig({ notes: description });
107
+ tree(path, `section-${index + 1}`, `${input.header.trim()}
108
+
109
+ ${preamble}
110
+
111
+ ${link("llms.txt", "/llms.txt")}`, section.title, entries);
112
+ sections.push({ ...section, sitemap: void 0, path: void 0, links: [{ title: section.title, href: input.resolveUrl(path), description }] });
113
+ }
114
+ const rootHeader = `${input.header.trim()}
115
+
116
+ ${normalizeLlmsTxtConfig({ ...input.config, sections })}${input.rootContent ? `
117
+
118
+ ${input.rootContent}` : ""}`;
119
+ const pages = input.pages.filter((page) => !assigned.has(page.pathname)).map(formatLlmsTxtPageLink);
120
+ if (!pages.length && rootHeader.length + 1 <= maxLength) {
121
+ files.set("/llms.txt", `${rootHeader.trim()}
122
+ `);
123
+ } else {
124
+ const inline = render(rootHeader, "Pages", pages);
125
+ if (inline.length <= maxLength) {
126
+ files.set("/llms.txt", inline);
127
+ } else {
128
+ tree("/llms/_pages.txt", "pages", input.header, "Pages", pages);
129
+ const root = `${rootHeader.trim()}
130
+
131
+ ## Pages
132
+
133
+ ${link("Pages", "/llms/_pages.txt")}
134
+ `;
135
+ if (root.length > maxLength)
136
+ throw new Error("llms.txt sections exceed llmsTxt.maxLength. Shorten the sections or increase the limit.");
137
+ files.set("/llms.txt", root);
138
+ }
139
+ }
140
+ return files;
141
+ }
@@ -1,4 +1,5 @@
1
1
  import type { H3Event } from '#nuxtseo/h3';
2
- import { normalizeLlmsTxtConfig } from './llms-txt-format.js';
3
- export { normalizeLlmsTxtConfig };
2
+ export { normalizeLlmsTxtConfig } from './llms-txt-format.js';
3
+ /** One discovery snapshot supplies the root and every bounded child file. */
4
+ export declare function buildLlmsTxtFiles(event: H3Event): Promise<Record<string, string>>;
4
5
  export declare function buildLlmsTxt(event: H3Event): Promise<string>;