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.
- package/dist/chunks/agent-skills.mjs +2 -1
- package/dist/chunks/prerender.mjs +189 -27
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/llms-txt-files.d.ts +30 -0
- package/dist/runtime/llms-txt-files.js +141 -0
- package/dist/runtime/llms-txt-utils.d.ts +3 -2
- package/dist/runtime/llms-txt-utils.js +85 -224
- package/dist/runtime/server/db/queries.d.ts +3 -0
- package/dist/runtime/server/db/queries.js +28 -2
- package/dist/runtime/server/middleware/markdown.js +1 -1
- package/dist/runtime/server/routes/llms-full.txt.get.js +2 -1
- package/dist/runtime/server/routes/llms.txt.get.js +19 -11
- package/dist/runtime/server/routes/sitemap.md.get.js +2 -1
- package/dist/runtime/server/utils/markdown-request.js +2 -5
- package/dist/runtime/server/utils/sitemap.d.ts +1 -0
- package/dist/runtime/server/utils/sitemap.js +22 -10
- package/dist/runtime/types.d.ts +13 -1
- package/dist/shared/{nuxt-ai-ready.DH9mSayy.mjs → nuxt-ai-ready.BwnSMqRn.mjs} +41 -12
- package/package.json +1 -1
- package/skills/nuxt-ai-ready/SKILL.md +34 -1
|
@@ -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.
|
|
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 {
|
|
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.
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
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.
|
|
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
|
-
|
|
3
|
-
|
|
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>;
|