nuxt-ai-ready 3.1.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 +196 -29
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/llms-txt-directive.d.ts +15 -0
- package/dist/runtime/llms-txt-directive.js +23 -0
- 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 -221
- package/dist/runtime/markdown-links.d.ts +10 -0
- package/dist/runtime/markdown-links.js +52 -0
- 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 +9 -2
- package/dist/runtime/server/middleware/markdown.prerender.js +2 -1
- package/dist/runtime/server/plugins/llms-txt-directive-html.d.ts +3 -0
- package/dist/runtime/server/plugins/llms-txt-directive-html.js +8 -0
- 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 +6 -3
- 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/server/utils.js +2 -1
- package/dist/runtime/types.d.ts +30 -2
- package/dist/shared/{nuxt-ai-ready.Dmvy7CIJ.mjs → nuxt-ai-ready.BwnSMqRn.mjs} +94 -28
- package/package.json +1 -1
- package/skills/nuxt-ai-ready/SKILL.md +37 -2
|
@@ -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,14 +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.
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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';
|
|
11
|
+
import { insertLlmsTxtDirective } from '../../dist/runtime/llms-txt-directive.js';
|
|
12
|
+
import { absoluteSiteUrl } from '../../dist/runtime/markdown-links.js';
|
|
13
|
+
import { normalizePagePath, hasMarkdownTwin, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
|
|
12
14
|
import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
|
|
13
15
|
import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../../dist/runtime/server/utils/llms-full.js';
|
|
14
16
|
import { resolvePageUpdatedAt } from '../../dist/runtime/server/utils/page-updated-at.js';
|
|
@@ -21,6 +23,7 @@ import 'nuxt-site-config/kit';
|
|
|
21
23
|
import 'nuxtseo-shared/kit';
|
|
22
24
|
import '../shared/nuxt-ai-ready.B3hAuOLQ.mjs';
|
|
23
25
|
import 'node:fs';
|
|
26
|
+
import '../../dist/runtime/llms-txt-files.js';
|
|
24
27
|
import '../../dist/runtime/cache-control.js';
|
|
25
28
|
import '../../dist/runtime/server/utils/discovery-response.js';
|
|
26
29
|
import 'confbox';
|
|
@@ -29,6 +32,33 @@ import '../../dist/runtime/server/utils/content-negotiation.js';
|
|
|
29
32
|
import 'pathe';
|
|
30
33
|
import 'nuxtseo-shared/i18n';
|
|
31
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
|
+
|
|
32
62
|
const PRERENDER_PAGE_TIMEOUT = 3e4;
|
|
33
63
|
const RE_HTML_MD_EXT = /\.(html|md)$/;
|
|
34
64
|
const RE_INDEX_SUFFIX = /\/index$/;
|
|
@@ -184,7 +214,32 @@ async function processMarkdownRoute(state, nuxt, route, parsed, lastmod, options
|
|
|
184
214
|
async function processSitemapEntry(state, nuxt, nitro, entry) {
|
|
185
215
|
const loc = typeof entry === "string" ? entry : entry.loc;
|
|
186
216
|
const lastmod = typeof entry === "string" ? void 0 : entry.lastmod;
|
|
187
|
-
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
|
+
}
|
|
188
243
|
if (route.split("/").some((segment) => segment.startsWith("_"))) {
|
|
189
244
|
return { crawled: false, skipped: true };
|
|
190
245
|
}
|
|
@@ -193,12 +248,24 @@ async function processSitemapEntry(state, nuxt, nitro, entry) {
|
|
|
193
248
|
}
|
|
194
249
|
const mdRoute = toMarkdownPath(route);
|
|
195
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 };
|
|
196
261
|
logger.debug(`Fetching markdown for ${route} \u2192 ${mdUrl}`);
|
|
197
262
|
const res = await crawlerFetch(state)(mdUrl, {
|
|
198
263
|
headers: { "x-nitro-prerender": mdRoute },
|
|
199
264
|
retry: 0,
|
|
200
265
|
signal: AbortSignal.timeout(PRERENDER_PAGE_TIMEOUT)
|
|
201
266
|
}).catch((err) => {
|
|
267
|
+
if (err?.response?.status === 404 || err?.response?.status === 410)
|
|
268
|
+
state.errorRoutes.add(route);
|
|
202
269
|
if (err?.name === "TimeoutError" || err?.name === "AbortError")
|
|
203
270
|
logger.warn(`Timeout (${PRERENDER_PAGE_TIMEOUT}ms) fetching markdown for ${route}`);
|
|
204
271
|
else
|
|
@@ -215,6 +282,29 @@ async function processSitemapEntry(state, nuxt, nitro, entry) {
|
|
|
215
282
|
return { crawled: false, skipped: false };
|
|
216
283
|
}
|
|
217
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
|
+
}
|
|
218
308
|
return { crawled: true, skipped: false };
|
|
219
309
|
}
|
|
220
310
|
async function crawlSitemapEntries(state, nuxt, nitro, entries) {
|
|
@@ -235,18 +325,72 @@ async function crawlSitemapEntries(state, nuxt, nitro, entries) {
|
|
|
235
325
|
logger.debug(`Sitemap crawl complete: ${crawled} crawled, ${skipped} skipped`);
|
|
236
326
|
return crawled;
|
|
237
327
|
}
|
|
238
|
-
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()) {
|
|
239
329
|
logger.debug(`Parsing sitemap XML (${sitemapContent.length} bytes)`);
|
|
240
330
|
const result = await collectSitemap(sitemapContent);
|
|
241
|
-
if (result._tag !== "document"
|
|
331
|
+
if (result._tag !== "document") {
|
|
242
332
|
const issues = result.issues.map((issue) => issue.message).join("; ");
|
|
243
333
|
logger.debug(`Skipping sitemap: ${issues || "document is not a URL set"}`);
|
|
244
334
|
return 0;
|
|
245
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
|
+
}
|
|
246
367
|
const urls = result.document.entries;
|
|
247
368
|
logger.debug(`Found ${urls.length} URLs in sitemap`);
|
|
248
369
|
return crawlSitemapEntries(state, nuxt, nitro, urls);
|
|
249
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
|
+
}
|
|
250
394
|
function isNuxtGenerate() {
|
|
251
395
|
return process.argv.includes("generate") || process.env.NUXT_GENERATE === "true" || process.env.prerender === "true";
|
|
252
396
|
}
|
|
@@ -272,6 +416,7 @@ function detectSitemapPrerender(sitemapName = "sitemap.xml") {
|
|
|
272
416
|
};
|
|
273
417
|
}
|
|
274
418
|
async function prerenderRoute(nitro, route, fetch) {
|
|
419
|
+
const filePath = resolvePrerenderFile(nitro.options.output.publicDir, route);
|
|
275
420
|
const start = Date.now();
|
|
276
421
|
const encodedRoute = encodeURI(route);
|
|
277
422
|
const fetchUrl = withBase(encodedRoute, nitro.options.baseURL);
|
|
@@ -280,7 +425,6 @@ async function prerenderRoute(nitro, route, fetch) {
|
|
|
280
425
|
retry: nitro.options.prerender.retry,
|
|
281
426
|
retryDelay: nitro.options.prerender.retryDelay
|
|
282
427
|
});
|
|
283
|
-
const filePath = join(nitro.options.output.publicDir, route);
|
|
284
428
|
logger.debug(`Creating directory for prerender: ${dirname(filePath)}`);
|
|
285
429
|
await mkdir(dirname(filePath), { recursive: true });
|
|
286
430
|
const data = res._data;
|
|
@@ -302,7 +446,7 @@ async function prerenderRoute(nitro, route, fetch) {
|
|
|
302
446
|
function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras) {
|
|
303
447
|
const nuxt = useNuxt();
|
|
304
448
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
305
|
-
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");
|
|
306
450
|
const state = createCrawlerState(
|
|
307
451
|
dbPath,
|
|
308
452
|
llmsFullTxtPath,
|
|
@@ -346,8 +490,10 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
|
|
|
346
490
|
initPromise = initCrawler(state);
|
|
347
491
|
await initPromise;
|
|
348
492
|
const parsed = JSON.parse(route.contents || "{}");
|
|
349
|
-
|
|
350
|
-
|
|
493
|
+
if (!state.prerenderedRoutes.has(pageRoute))
|
|
494
|
+
await processMarkdownRoute(state, nuxt, pageRoute, parsed);
|
|
495
|
+
const withSitemap = options.sitemapMd === false ? parsed.markdown : appendSitemapSection(parsed.markdown, absoluteSiteUrl(toDeployedRoute(SITEMAP_MD_ROUTE, nitro.options.baseURL), siteInfo?.url));
|
|
496
|
+
route.contents = llmsTxtConfig?.directive?.markdown === false ? withSitemap : insertLlmsTxtDirective(withSitemap, absoluteSiteUrl(toDeployedRoute("/llms.txt", nitro.options.baseURL), siteInfo?.url));
|
|
351
497
|
route.contentType = "text/markdown; charset=utf-8";
|
|
352
498
|
state.totalProcessingTime += Date.now() - pageStartTime;
|
|
353
499
|
});
|
|
@@ -409,10 +555,12 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
|
|
|
409
555
|
}
|
|
410
556
|
if (state.dbPath && state.llmsTxtConfig?.markdownLinks) {
|
|
411
557
|
const availabilityFile = join(dirname(state.dbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
|
|
558
|
+
const options2 = nitro.options;
|
|
559
|
+
const ignoreBaseDir = options2.srcDir || (typeof options2.serverDir === "string" ? options2.serverDir : void 0) || options2.rootDir;
|
|
412
560
|
const paths = await findOutputMarkdownPaths(
|
|
413
561
|
nitro.options.output.publicDir,
|
|
414
562
|
nitro.options.baseURL,
|
|
415
|
-
|
|
563
|
+
ignoreBaseDir,
|
|
416
564
|
nitro.options.ignore,
|
|
417
565
|
nitro.options.publicAssets
|
|
418
566
|
);
|
|
@@ -421,20 +569,46 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
|
|
|
421
569
|
paths
|
|
422
570
|
}), "utf-8");
|
|
423
571
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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" });
|
|
427
601
|
if (options.sitemapMd !== false) {
|
|
428
602
|
const sitemapMdStats = await prerenderRoute(nitro, SITEMAP_MD_ROUTE, crawlerFetch(state));
|
|
429
603
|
logger.debug(`Wrote sitemap.md (${(sitemapMdStats.size / 1024).toFixed(1)}kb)`);
|
|
430
604
|
}
|
|
431
605
|
const kb = (b) => (b / 1024).toFixed(1);
|
|
432
|
-
const totalKb = kb(
|
|
606
|
+
const totalKb = kb(llmsSize + llmsFullStats.size);
|
|
433
607
|
const dim = (s) => colorize("dim", s);
|
|
434
608
|
const cyan = (s) => colorize("cyan", s);
|
|
435
609
|
const timeStr = state.totalProcessingTime >= 100 ? ` in ${cyan(`${(state.totalProcessingTime / 1e3).toFixed(1)}s`)}` : "";
|
|
436
610
|
logger.info(`Indexed ${cyan(String(state.prerenderedRoutes.size))} pages for llms.txt${timeStr} \u2192 ${cyan(`${totalKb}kb`)}`);
|
|
437
|
-
logger.info(dim(` llms.txt: ${kb(
|
|
611
|
+
logger.info(dim(` llms.txt files: ${kb(llmsSize)}kb, llms-full.txt: ${kb(llmsFullStats.size)}kb`));
|
|
438
612
|
}
|
|
439
613
|
const { useSitemapHook, usePrerenderHook } = detectSitemapPrerender();
|
|
440
614
|
logger.debug(`Prerender hooks: sitemap=${useSitemapHook}, prerender=${usePrerenderHook}`);
|
|
@@ -442,8 +616,8 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
|
|
|
442
616
|
nuxt.hooks.hook("sitemap:prerender:done", async (ctx) => (async () => {
|
|
443
617
|
if (!state.initialized)
|
|
444
618
|
return;
|
|
445
|
-
|
|
446
|
-
|
|
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);
|
|
447
621
|
await writeLlmsFiles();
|
|
448
622
|
state.prerenderedRoutes.clear();
|
|
449
623
|
if (state.db)
|
|
@@ -453,14 +627,7 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
|
|
|
453
627
|
nitro.hooks.hook("prerender:done", async () => (async () => {
|
|
454
628
|
if (!state.initialized)
|
|
455
629
|
return;
|
|
456
|
-
|
|
457
|
-
headers: { "x-nitro-prerender": "/sitemap.xml" },
|
|
458
|
-
signal: AbortSignal.timeout(PRERENDER_PAGE_TIMEOUT)
|
|
459
|
-
}).catch(() => {
|
|
460
|
-
return null;
|
|
461
|
-
});
|
|
462
|
-
if (sitemapContent)
|
|
463
|
-
await crawlSitemapContent(state, nuxt, nitro, sitemapContent);
|
|
630
|
+
await crawlConfiguredSitemaps(state, nuxt, nitro, nuxt.options.sitemap || {});
|
|
464
631
|
await writeLlmsFiles();
|
|
465
632
|
state.prerenderedRoutes.clear();
|
|
466
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,15 @@
|
|
|
1
|
+
/** The line that points a reader of one page at the whole site's index. */
|
|
2
|
+
export declare function llmsTxtDirective(llmsTxtUrl: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Add the llms.txt directive near the top of a page's Markdown, after any
|
|
5
|
+
* frontmatter so YAML parsers still find it first. An agent that lands on one
|
|
6
|
+
* page then knows where the full index is.
|
|
7
|
+
*/
|
|
8
|
+
export declare function insertLlmsTxtDirective(markdown: string, llmsTxtUrl: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* The HTML equivalent: a link at the top of the body, hidden visually but
|
|
11
|
+
* kept for screen readers and agents that read the raw HTML.
|
|
12
|
+
*/
|
|
13
|
+
export declare function llmsTxtDirectiveHtml(llmsTxtPath: string): string;
|
|
14
|
+
/** Remove the injected HTML link, so a page's Markdown carries the directive once. */
|
|
15
|
+
export declare function stripLlmsTxtDirectiveHtml(html: string): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const RE_FRONTMATTER = /^\uFEFF?---[\t ]*\r?\n(?:[\s\S]*?\r?\n)?---[\t ]*(?:\r?\n|$)/;
|
|
2
|
+
export function llmsTxtDirective(llmsTxtUrl) {
|
|
3
|
+
return `> Index of every page on this site: [llms.txt](${llmsTxtUrl})`;
|
|
4
|
+
}
|
|
5
|
+
export function insertLlmsTxtDirective(markdown, llmsTxtUrl) {
|
|
6
|
+
const directive = llmsTxtDirective(llmsTxtUrl);
|
|
7
|
+
const frontmatter = markdown.match(RE_FRONTMATTER)?.[0] ?? "";
|
|
8
|
+
const body = markdown.slice(frontmatter.length).replace(/^(?:\r?\n)+/, "");
|
|
9
|
+
if (body === directive || body.startsWith(`${directive}
|
|
10
|
+
`) || body.startsWith(`${directive}\r
|
|
11
|
+
`))
|
|
12
|
+
return markdown;
|
|
13
|
+
return `${frontmatter}${frontmatter ? "\n" : ""}${directive}
|
|
14
|
+
|
|
15
|
+
${body}`;
|
|
16
|
+
}
|
|
17
|
+
const RE_DIRECTIVE_HTML = /<a [^>]*data-llms-txt-directive[^>]*>[^<]*<\/a>/g;
|
|
18
|
+
export function llmsTxtDirectiveHtml(llmsTxtPath) {
|
|
19
|
+
return `<a href="${llmsTxtPath}" tabindex="-1" style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0" data-llms-txt-directive>Index of every page on this site: llms.txt</a>`;
|
|
20
|
+
}
|
|
21
|
+
export function stripLlmsTxtDirectiveHtml(html) {
|
|
22
|
+
return html.replace(RE_DIRECTIVE_HTML, "");
|
|
23
|
+
}
|
|
@@ -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>;
|