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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { negotiateContent, parseAcceptHeader } from "@mdream/js/negotiate";
|
|
2
2
|
import { getBotInfo } from "@nuxtjs/robots/util";
|
|
3
3
|
import { getHeaders } from "#nuxtseo/h3";
|
|
4
|
-
import { isReservedPath } from "../../markdown-path.js";
|
|
4
|
+
import { hasMarkdownTwin, isReservedPath } from "../../markdown-path.js";
|
|
5
5
|
const RE_MD_EXT = /\.md$/;
|
|
6
6
|
export function toMarkdownRequest(event, isPrerender = !!import.meta.prerender) {
|
|
7
7
|
return { path: event.path, headers: getHeaders(event), isPrerender };
|
|
@@ -38,10 +38,7 @@ export function getRequestRenderInfo(request, mode = { _tag: "runtime", contentN
|
|
|
38
38
|
const isExplicit = originalPath.endsWith(".md");
|
|
39
39
|
if (isPrerender && !isExplicit)
|
|
40
40
|
return null;
|
|
41
|
-
|
|
42
|
-
const hasExtension = lastSegment.includes(".");
|
|
43
|
-
const extension = hasExtension ? lastSegment.substring(lastSegment.lastIndexOf(".")) : "";
|
|
44
|
-
if (hasExtension && extension !== ".md")
|
|
41
|
+
if (!isExplicit && !hasMarkdownTwin(originalPath))
|
|
45
42
|
return null;
|
|
46
43
|
const negotiation = isPrerender ? "markdown" : mode.contentNegotiation ? negotiateRequestRepresentation(request, mode.botNegotiation) : "html";
|
|
47
44
|
if (isExplicit) {
|
|
@@ -53,5 +53,6 @@ export declare function fetchSitemapByRoute(event: H3Event | undefined, route: s
|
|
|
53
53
|
* Fetch all URLs from all sitemaps (or single sitemap if not multi-sitemap)
|
|
54
54
|
* Used for backwards compatibility and llms.txt generation
|
|
55
55
|
*/
|
|
56
|
+
export declare function fetchSitemapUrlGroups(event: H3Event, names?: readonly string[]): Promise<Map<string, SitemapUrl[]>>;
|
|
56
57
|
export declare function fetchSitemapUrls(event: H3Event): Promise<SitemapUrl[]>;
|
|
57
58
|
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSitemapReader } from "sitemapd";
|
|
2
2
|
import { parseSitemap } from "sitemapd/parse";
|
|
3
|
+
import staticSitemapConfig from "#ai-ready-virtual/sitemap-config.mjs";
|
|
4
|
+
import { getRequestURL } from "#nuxtseo/h3";
|
|
3
5
|
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
4
6
|
import { logger } from "../logger.js";
|
|
5
7
|
import { getCfEnv, hasAssets } from "./cloudflare.js";
|
|
@@ -19,17 +21,18 @@ const SITEMAP_READER_LIMITS = {
|
|
|
19
21
|
const MAX_SITEMAP_CRAWL_ROUNDS = 10;
|
|
20
22
|
export function getSitemapsFromConfig(event) {
|
|
21
23
|
const runtimeConfig = useRuntimeConfig(event);
|
|
22
|
-
const sitemapConfig = runtimeConfig.sitemap;
|
|
24
|
+
const sitemapConfig = { ...staticSitemapConfig, ...runtimeConfig.sitemap };
|
|
23
25
|
if (!sitemapConfig?.sitemaps)
|
|
24
26
|
return [];
|
|
25
27
|
const sitemaps = [];
|
|
26
28
|
for (const [key, sitemap] of Object.entries(sitemapConfig.sitemaps)) {
|
|
27
29
|
if (key === "index")
|
|
28
30
|
continue;
|
|
29
|
-
|
|
31
|
+
const route = sitemap._route || sitemap.route || (key === "chunks" ? sitemapConfig.sitemaps.index?._route || sitemapConfig.sitemaps.index?.route : void 0);
|
|
32
|
+
if (route) {
|
|
30
33
|
sitemaps.push({
|
|
31
|
-
name: sitemap.sitemapName || key,
|
|
32
|
-
route:
|
|
34
|
+
name: (sitemap.sitemapName || key).replace(/\.xml$/, ""),
|
|
35
|
+
route: route.startsWith("/") ? route : `/${route}`
|
|
33
36
|
});
|
|
34
37
|
}
|
|
35
38
|
}
|
|
@@ -305,6 +308,7 @@ export async function crawlSitemapByRoute(event, input) {
|
|
|
305
308
|
const usePublicAsset = config.sitemapPrerendered && hasAssets(event);
|
|
306
309
|
const configuredSiteUrl = createUniversalContext(event).siteUrl ?? SITEMAP_FALLBACK_ORIGIN;
|
|
307
310
|
const siteUrl = parseAbsoluteUrl(configuredSiteUrl);
|
|
311
|
+
const requestOrigin = import.meta.dev && event ? getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).origin : void 0;
|
|
308
312
|
if (siteUrl._tag === "error") {
|
|
309
313
|
return {
|
|
310
314
|
_tag: "failed",
|
|
@@ -346,7 +350,7 @@ export async function crawlSitemapByRoute(event, input) {
|
|
|
346
350
|
const target = parseAbsoluteUrl(request.url);
|
|
347
351
|
if (target._tag === "error")
|
|
348
352
|
return { _tag: "deny", reason: `Invalid sitemap target: ${request.url}` };
|
|
349
|
-
if (target.value.origin !== siteUrl.value.origin) {
|
|
353
|
+
if (target.value.origin !== siteUrl.value.origin && target.value.origin !== requestOrigin) {
|
|
350
354
|
return {
|
|
351
355
|
_tag: "deny",
|
|
352
356
|
reason: `Sitemap target origin ${target.value.origin} differs from ${siteUrl.value.origin}`
|
|
@@ -433,17 +437,21 @@ export async function fetchSitemapByRoute(event, route) {
|
|
|
433
437
|
const result = await crawlSitemapByRoute(event, { route });
|
|
434
438
|
return result._tag === "complete" ? { urls: result.urls } : { urls: result.urls, error: result.error };
|
|
435
439
|
}
|
|
436
|
-
export async function
|
|
440
|
+
export async function fetchSitemapUrlGroups(event, names) {
|
|
437
441
|
const sitemaps = getSitemapsFromConfig(event);
|
|
442
|
+
const groups = /* @__PURE__ */ new Map();
|
|
438
443
|
if (sitemaps.length > 0) {
|
|
439
444
|
logger.debug(`[sitemap] Multi-sitemap mode: ${sitemaps.length} sitemaps`);
|
|
440
|
-
const allUrls = [];
|
|
441
445
|
for (const sitemap of sitemaps) {
|
|
446
|
+
if (names !== void 0 && !names.includes(sitemap.name))
|
|
447
|
+
continue;
|
|
442
448
|
const { urls: urls2 } = await fetchSitemapByRoute(event, sitemap.route);
|
|
443
|
-
|
|
449
|
+
groups.set(sitemap.name, [...groups.get(sitemap.name) || [], ...urls2]);
|
|
444
450
|
}
|
|
445
|
-
return
|
|
451
|
+
return groups;
|
|
446
452
|
}
|
|
453
|
+
if (names !== void 0 && !names.includes("sitemap"))
|
|
454
|
+
return groups;
|
|
447
455
|
logger.debug(`[sitemap] Single sitemap mode: /sitemap.xml`);
|
|
448
456
|
const { urls, error } = await fetchSitemapByRoute(event, "/sitemap.xml");
|
|
449
457
|
if (error) {
|
|
@@ -451,5 +459,9 @@ export async function fetchSitemapUrls(event) {
|
|
|
451
459
|
} else if (urls.length === 0) {
|
|
452
460
|
logger.warn("Sitemap is empty - add routes to sitemap or configure sitemap.sources in nuxt.config");
|
|
453
461
|
}
|
|
454
|
-
|
|
462
|
+
groups.set("sitemap", urls);
|
|
463
|
+
return groups;
|
|
464
|
+
}
|
|
465
|
+
export async function fetchSitemapUrls(event) {
|
|
466
|
+
return [...(await fetchSitemapUrlGroups(event)).values()].flat();
|
|
455
467
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { htmlToMarkdown } from "mdream";
|
|
2
2
|
import { useNitroApp } from "#nuxtseo/nitro";
|
|
3
|
+
import { stripLlmsTxtDirectiveHtml } from "../llms-txt-directive.js";
|
|
3
4
|
import { hoistMetaDescription } from "./utils/frontmatter.js";
|
|
4
5
|
const RE_NBSP = /\u00A0/g;
|
|
5
6
|
function normalizeWhitespace(text) {
|
|
@@ -75,7 +76,7 @@ export async function convertHtmlToMarkdown(html, url, mdreamOptions, opts) {
|
|
|
75
76
|
await nitroApp.hooks.callHook("ai-ready:mdreamConfig", options);
|
|
76
77
|
const context = {
|
|
77
78
|
html,
|
|
78
|
-
markdown: hoistMetaDescription(htmlToMarkdown(html, options)),
|
|
79
|
+
markdown: hoistMetaDescription(htmlToMarkdown(stripLlmsTxtDirectiveHtml(html), options)),
|
|
79
80
|
route: opts.hooks.route,
|
|
80
81
|
title: meta.title,
|
|
81
82
|
description: meta.description,
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -185,7 +185,9 @@ export interface ModuleOptions {
|
|
|
185
185
|
/**
|
|
186
186
|
* Structured llms.txt configuration
|
|
187
187
|
*/
|
|
188
|
-
llmsTxt?: LlmsTxtConfig;
|
|
188
|
+
llmsTxt?: LlmsTxtConfig | false;
|
|
189
|
+
/** Generate and serve llms-full.txt. Set false to omit the export and its discovery link. */
|
|
190
|
+
llmsFullTxt?: boolean;
|
|
189
191
|
/**
|
|
190
192
|
* Generate a Markdown sitemap at `/sitemap.md` from indexed pages and end
|
|
191
193
|
* every Markdown page with a `## Sitemap` section linking to it.
|
|
@@ -512,6 +514,10 @@ export interface LlmsTxtLink {
|
|
|
512
514
|
* Section in llms.txt
|
|
513
515
|
*/
|
|
514
516
|
export interface LlmsTxtSection {
|
|
517
|
+
/** Generate a separate file from these sitemap names. Chunked sitemaps retain their configured name. */
|
|
518
|
+
sitemap?: string | string[];
|
|
519
|
+
/** Generated file path. Defaults to /llms/<sitemap>.txt. */
|
|
520
|
+
path?: string;
|
|
515
521
|
/** The title of the section */
|
|
516
522
|
title: string;
|
|
517
523
|
/** Description rendered in the heading-free preamble, or in each optional link note (can be array for multiple paragraphs) */
|
|
@@ -525,13 +531,35 @@ export interface LlmsTxtSection {
|
|
|
525
531
|
* Structured llms.txt configuration
|
|
526
532
|
*/
|
|
527
533
|
export interface LlmsTxtConfig {
|
|
534
|
+
/** Page admission. Sitemap URLs are authoritative by default. Use all to include stored pages outside the sitemap. */
|
|
535
|
+
pages?: 'sitemap' | 'all' | false;
|
|
536
|
+
/** Only read these sitemap names. An empty array disables sitemap page discovery. */
|
|
537
|
+
sitemaps?: string[];
|
|
538
|
+
/** Maximum characters per generated file. Large lists become linked files. Minimum 1024. Default 80000. */
|
|
539
|
+
maxLength?: number;
|
|
528
540
|
/**
|
|
529
541
|
* Rewrite automatically generated page links to their Markdown representation
|
|
530
542
|
* when the final static output contains the file or the deployment retains
|
|
531
543
|
* the runtime Markdown handler.
|
|
532
|
-
* @default
|
|
544
|
+
* @default true
|
|
533
545
|
*/
|
|
534
546
|
markdownLinks?: boolean;
|
|
547
|
+
/**
|
|
548
|
+
* Point each page at llms.txt, so an agent that lands on one page finds the
|
|
549
|
+
* whole site's index.
|
|
550
|
+
*/
|
|
551
|
+
directive?: {
|
|
552
|
+
/**
|
|
553
|
+
* Add a blockquote line after the frontmatter of every page's `.md` response.
|
|
554
|
+
* @default true
|
|
555
|
+
*/
|
|
556
|
+
markdown?: boolean;
|
|
557
|
+
/**
|
|
558
|
+
* Add a visually hidden link to `/llms.txt` at the top of every HTML page's body.
|
|
559
|
+
* @default false
|
|
560
|
+
*/
|
|
561
|
+
html?: boolean;
|
|
562
|
+
};
|
|
535
563
|
/** The sections of the documentation */
|
|
536
564
|
sections?: LlmsTxtSection[];
|
|
537
565
|
/** Additional heading-free preamble notes rendered before file-list sections */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { readFile, mkdir, writeFile
|
|
2
|
+
import { readFile, access, mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { dirname, basename, join, resolve, relative } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -8,6 +8,7 @@ import defu$1, { defu } from 'defu';
|
|
|
8
8
|
import { useSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
|
|
9
9
|
import { resolveNuxtContentVersion, renderNitroTypeAugmentations, setupRuntimeAliases, setupNitroRuntimeCompatibility } from 'nuxtseo-shared/kit';
|
|
10
10
|
import { r as readPackageMetadata, a as resolvePackageMetadataPath } from './nuxt-ai-ready.B3hAuOLQ.mjs';
|
|
11
|
+
import { parseLlmsTxtConfig } from '../../dist/runtime/llms-txt-files.js';
|
|
11
12
|
import { toDeployedRoute } from '../../dist/runtime/route-path.js';
|
|
12
13
|
import { SITEMAP_MD_ROUTE } from '../../dist/runtime/server/utils/sitemap-md.js';
|
|
13
14
|
import { publicCacheControl, mergeVaryHeader } from '../../dist/runtime/cache-control.js';
|
|
@@ -210,7 +211,11 @@ export {}
|
|
|
210
211
|
});
|
|
211
212
|
addTemplate({
|
|
212
213
|
filename: "types/ai-ready-virtual.d.ts",
|
|
213
|
-
getContents: () => `declare module '#ai-ready-virtual/
|
|
214
|
+
getContents: () => `declare module '#ai-ready-virtual/sitemap-config.mjs' {
|
|
215
|
+
const config: { sitemaps?: Record<string, { sitemapName?: string, route?: string, _route?: string }> }
|
|
216
|
+
export default config
|
|
217
|
+
}
|
|
218
|
+
declare module '#ai-ready-virtual/read-page-data.mjs' {
|
|
214
219
|
export function readMarkdownLinkAvailabilityFromFilesystem(): Promise<{
|
|
215
220
|
runtimeMarkdownAvailable: boolean
|
|
216
221
|
paths: string[]
|
|
@@ -666,18 +671,42 @@ function cloudflareStaticFallbackPaths(existing, pages, environments = []) {
|
|
|
666
671
|
const patterns = [...existing || [], ...environments.flatMap((routing) => Array.isArray(routing) ? routing : [])];
|
|
667
672
|
return pagePaths(pages).filter((path) => !patterns.some((pattern) => matchesPattern(path, pattern.startsWith("!") ? pattern.slice(1) : pattern)));
|
|
668
673
|
}
|
|
669
|
-
function
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
674
|
+
function parentDirectories(path) {
|
|
675
|
+
const segments = path.split("/").filter(Boolean);
|
|
676
|
+
return segments.slice(0, -1).map((_, index) => `/${segments.slice(0, index + 1).join("/")}/`);
|
|
677
|
+
}
|
|
678
|
+
function fitWorkerFirstBudget(paths, budget) {
|
|
679
|
+
let remaining = [...paths];
|
|
680
|
+
const globs = [];
|
|
681
|
+
while (remaining.length + globs.length > budget) {
|
|
682
|
+
const counts = /* @__PURE__ */ new Map();
|
|
683
|
+
for (const path of remaining) {
|
|
684
|
+
for (const directory2 of parentDirectories(path))
|
|
685
|
+
counts.set(directory2, (counts.get(directory2) ?? 0) + 1);
|
|
686
|
+
}
|
|
687
|
+
let best;
|
|
688
|
+
for (const [directory2, count] of counts) {
|
|
689
|
+
if (!best || count > best[1] || count === best[1] && directory2.length > best[0].length)
|
|
690
|
+
best = [directory2, count];
|
|
691
|
+
}
|
|
692
|
+
if (!best || best[1] < 2)
|
|
693
|
+
return void 0;
|
|
694
|
+
const [directory] = best;
|
|
695
|
+
globs.push(`${directory}*`);
|
|
696
|
+
remaining = remaining.filter((path) => !path.startsWith(directory) || path === directory);
|
|
676
697
|
}
|
|
677
|
-
|
|
678
|
-
|
|
698
|
+
return [...remaining, ...globs];
|
|
699
|
+
}
|
|
700
|
+
function cloudflareWorkerFirstAdditions(existing, pages) {
|
|
701
|
+
if (existing === true || existing === false || pages.length === 0)
|
|
702
|
+
return [];
|
|
703
|
+
const patterns = existing || [];
|
|
704
|
+
const paths = cloudflareStaticFallbackPaths(existing, pages).filter((path) => !patterns.includes(path));
|
|
705
|
+
const additions = fitWorkerFirstBudget(paths, MAX_WORKER_FIRST_PATTERNS - patterns.length);
|
|
706
|
+
if (!additions) {
|
|
707
|
+
throw new Error("Cloudflare Markdown negotiation exceeds 100 worker-first patterns, even with directory globs. Configure broader assets.run_worker_first patterns, or disable aiReady.contentNegotiation.");
|
|
679
708
|
}
|
|
680
|
-
return
|
|
709
|
+
return additions;
|
|
681
710
|
}
|
|
682
711
|
|
|
683
712
|
const CACHE_GUIDE = "https://nuxtseo.com/docs/ai-ready/guides/markdown#cache-safety";
|
|
@@ -1219,6 +1248,14 @@ function createMcpServerCardEtag(card) {
|
|
|
1219
1248
|
return `"${createHash("sha256").update(JSON.stringify(card)).digest("hex")}"`;
|
|
1220
1249
|
}
|
|
1221
1250
|
|
|
1251
|
+
function opaqueJsonModule(exports) {
|
|
1252
|
+
const encode = (value) => JSON.stringify(Buffer.from(JSON.stringify(value), "utf8").toString("base64"));
|
|
1253
|
+
return [
|
|
1254
|
+
"const decode = value => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value), char => char.charCodeAt(0))))",
|
|
1255
|
+
...Object.entries(exports).map(([name, value]) => `export const ${name} = decode(${encode(value)})`)
|
|
1256
|
+
].join("\n");
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1222
1259
|
function escapeRegExp(value) {
|
|
1223
1260
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1224
1261
|
}
|
|
@@ -1808,7 +1845,7 @@ ${details}`);
|
|
|
1808
1845
|
const llmsFullRoute = withSiteUrl("llms-full.txt", { withBase: true });
|
|
1809
1846
|
defaultLlmsTxtSections.push({
|
|
1810
1847
|
title: "LLM Resources",
|
|
1811
|
-
links: [
|
|
1848
|
+
links: config.llmsFullTxt === false ? [] : [
|
|
1812
1849
|
{
|
|
1813
1850
|
title: "Full Content",
|
|
1814
1851
|
href: llmsFullRoute,
|
|
@@ -1821,21 +1858,39 @@ ${details}`);
|
|
|
1821
1858
|
defaultLlmsTxtSections.push({
|
|
1822
1859
|
title: "Agent Skills",
|
|
1823
1860
|
description: `Skills an agent can install from this site. The index at ${indexUrl} carries a sha256 digest for each one.`,
|
|
1824
|
-
links: agentSkillsResult.links.map((link) => ({
|
|
1861
|
+
links: [{ title: "Agent Skills index", href: indexUrl, description: "Every published skill and its sha256 digest." }, ...agentSkillsResult.links.map((link) => ({
|
|
1825
1862
|
title: link.name,
|
|
1826
1863
|
href: link.source === "local" ? withSiteUrl(link.href.slice(1), { withBase: true }) : resolveExternalSkillUrl(link.href, indexUrl),
|
|
1827
1864
|
description: link.description
|
|
1828
|
-
}))
|
|
1865
|
+
}))]
|
|
1829
1866
|
});
|
|
1830
1867
|
}
|
|
1868
|
+
const directiveConfig = config.llmsTxt === false ? void 0 : config.llmsTxt?.directive;
|
|
1869
|
+
const llmsTxtDirective = {
|
|
1870
|
+
markdown: directiveConfig?.markdown !== false,
|
|
1871
|
+
html: directiveConfig?.html === true
|
|
1872
|
+
};
|
|
1873
|
+
const parsedLlmsTxt = parseLlmsTxtConfig(config.llmsTxt || {});
|
|
1874
|
+
if (parsedLlmsTxt._tag === "Invalid")
|
|
1875
|
+
throw new Error(`[nuxt-ai-ready] ${parsedLlmsTxt.message}`);
|
|
1876
|
+
const publicLlmsTxt = await Promise.all(getLayerDirectories(nuxt).map(
|
|
1877
|
+
(layer) => access(join(layer.public, "llms.txt")).then(() => true).catch((error) => {
|
|
1878
|
+
if (error.code === "ENOENT")
|
|
1879
|
+
return false;
|
|
1880
|
+
throw error;
|
|
1881
|
+
})
|
|
1882
|
+
));
|
|
1883
|
+
const generateLlmsTxt = config.llmsTxt !== false && !publicLlmsTxt.some(Boolean);
|
|
1831
1884
|
const mergedLlmsTxt = config.llmsTxt ? {
|
|
1832
|
-
|
|
1885
|
+
...parsedLlmsTxt.config,
|
|
1886
|
+
markdownLinks: config.llmsTxt.markdownLinks ?? true,
|
|
1887
|
+
directive: llmsTxtDirective,
|
|
1833
1888
|
sections: [
|
|
1834
1889
|
...defaultLlmsTxtSections,
|
|
1835
1890
|
...config.llmsTxt.sections || []
|
|
1836
1891
|
],
|
|
1837
1892
|
notes: config.llmsTxt.notes
|
|
1838
|
-
} : { markdownLinks:
|
|
1893
|
+
} : { markdownLinks: true, directive: llmsTxtDirective, sections: defaultLlmsTxtSections };
|
|
1839
1894
|
const llmsTxtPayload = {
|
|
1840
1895
|
sections: mergedLlmsTxt.sections || [],
|
|
1841
1896
|
notes: typeof mergedLlmsTxt.notes === "string" ? [mergedLlmsTxt.notes] : mergedLlmsTxt.notes || []
|
|
@@ -1843,6 +1898,9 @@ ${details}`);
|
|
|
1843
1898
|
await nuxt.callHook("ai-ready:llms-txt", llmsTxtPayload);
|
|
1844
1899
|
mergedLlmsTxt.sections = llmsTxtPayload.sections;
|
|
1845
1900
|
mergedLlmsTxt.notes = llmsTxtPayload.notes.length > 0 ? llmsTxtPayload.notes : void 0;
|
|
1901
|
+
const parsedHookLlmsTxt = parseLlmsTxtConfig(mergedLlmsTxt);
|
|
1902
|
+
if (parsedHookLlmsTxt._tag === "Invalid")
|
|
1903
|
+
throw new Error(`[nuxt-ai-ready] ${parsedHookLlmsTxt.message}`);
|
|
1846
1904
|
const prerenderCacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt-seo/ai-ready/routes");
|
|
1847
1905
|
const buildDbPath = join(nuxt.options.buildDir, ".data/ai-ready/build.db");
|
|
1848
1906
|
const runtimeSyncConfig = typeof config.runtimeSync === "object" ? config.runtimeSync : {};
|
|
@@ -1859,6 +1917,8 @@ ${details}`);
|
|
|
1859
1917
|
if (apiCatalogRegistered)
|
|
1860
1918
|
return;
|
|
1861
1919
|
apiCatalogRegistered = true;
|
|
1920
|
+
const resources = mergedLlmsTxt.sections?.find((section) => section.title === "LLM Resources");
|
|
1921
|
+
resources?.links?.push({ title: "API catalog", href: catalog.href, description: "The public API and service descriptions." });
|
|
1862
1922
|
addServerHandler({ route: API_CATALOG_PATH, handler: resolve$1("./runtime/server/routes/api-catalog"), lazy: true });
|
|
1863
1923
|
const apiCatalogLink = `<${catalog.href}>; rel="api-catalog"`;
|
|
1864
1924
|
extendRouteRules("/**", {
|
|
@@ -2160,8 +2220,7 @@ export { unavailable as computeLocaleAlternates, unavailable as localePath, unav
|
|
|
2160
2220
|
`;
|
|
2161
2221
|
};
|
|
2162
2222
|
nitroConfig.virtual["#ai-ready-virtual/site-tools.mjs"] = `export default ${JSON.stringify(siteToolsConfig)}`;
|
|
2163
|
-
nitroConfig.virtual["#ai-ready-virtual/agent-skills.mjs"] = agentSkillsResult._tag === "Enabled" ?
|
|
2164
|
-
export const localAgentSkillArtifacts = ${JSON.stringify(agentSkillsResult.localArtifacts)}` : "export const agentSkillsIndex = null\nexport const localAgentSkillArtifacts = {}";
|
|
2223
|
+
nitroConfig.virtual["#ai-ready-virtual/agent-skills.mjs"] = agentSkillsResult._tag === "Enabled" ? opaqueJsonModule({ agentSkillsIndex: agentSkillsResult.index, localAgentSkillArtifacts: agentSkillsResult.localArtifacts }) : "export const agentSkillsIndex = null\nexport const localAgentSkillArtifacts = {}";
|
|
2165
2224
|
const markdownLinkAvailabilityPath = join(dirname(buildDbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
|
|
2166
2225
|
nitroConfig.virtual["#ai-ready-virtual/read-page-data.mjs"] = createBuildPageDataVirtual({
|
|
2167
2226
|
buildDbPath,
|
|
@@ -2237,6 +2296,7 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2237
2296
|
cron: !!config.cron
|
|
2238
2297
|
})}`;
|
|
2239
2298
|
nitroConfig.virtual["#ai-ready-virtual/content-lookup.mjs"] = contentLookupModule(contentSource);
|
|
2299
|
+
nitroConfig.virtual["#ai-ready-virtual/sitemap-config.mjs"] = `export { default } from '#sitemap-virtual/static-config.mjs'`;
|
|
2240
2300
|
});
|
|
2241
2301
|
nuxt.options.runtimeConfig["nuxt-ai-ready"] = {
|
|
2242
2302
|
version: version || "0.0.0",
|
|
@@ -2252,6 +2312,7 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2252
2312
|
swr: true
|
|
2253
2313
|
}),
|
|
2254
2314
|
llmsTxt: mergedLlmsTxt,
|
|
2315
|
+
llmsFullTxt: config.llmsFullTxt !== false,
|
|
2255
2316
|
llmsTxtCacheSeconds: config.llmsTxtCacheSeconds ?? 600,
|
|
2256
2317
|
prerenderCacheDir,
|
|
2257
2318
|
database,
|
|
@@ -2293,14 +2354,20 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2293
2354
|
}
|
|
2294
2355
|
addNitroPlugin(resolve$1(getNitroVersion(nuxt) === 3 ? "./runtime/server/plugins/link-header-nitro3" : "./runtime/server/plugins/link-header"));
|
|
2295
2356
|
addNitroPlugin(resolve$1("./runtime/server/plugins/markdown-negotiation"));
|
|
2357
|
+
if (llmsTxtDirective.html)
|
|
2358
|
+
addNitroPlugin(resolve$1("./runtime/server/plugins/llms-txt-directive-html"));
|
|
2296
2359
|
addPlugin({
|
|
2297
2360
|
mode: "server",
|
|
2298
2361
|
src: resolve$1("./runtime/app/plugins/md-alternate.server")
|
|
2299
2362
|
});
|
|
2300
|
-
|
|
2301
|
-
|
|
2363
|
+
if (generateLlmsTxt) {
|
|
2364
|
+
for (const route of ["/llms.txt", "/llms/**"])
|
|
2365
|
+
addServerHandler({ route, method: void 0, handler: resolve$1("./runtime/server/routes/llms.txt.get"), lazy: true });
|
|
2366
|
+
}
|
|
2367
|
+
if (config.llmsFullTxt !== false)
|
|
2368
|
+
addServerHandler({ route: "/llms-full.txt", method: void 0, handler: resolve$1("./runtime/server/routes/llms-full.txt.get"), lazy: true });
|
|
2302
2369
|
if (config.sitemapMd !== false) {
|
|
2303
|
-
addServerHandler({ route: SITEMAP_MD_ROUTE, handler: resolve$1("./runtime/server/routes/sitemap.md.get"), lazy: true });
|
|
2370
|
+
addServerHandler({ route: SITEMAP_MD_ROUTE, method: void 0, handler: resolve$1("./runtime/server/routes/sitemap.md.get"), lazy: true });
|
|
2304
2371
|
}
|
|
2305
2372
|
if (agentSkillsResult._tag === "Enabled") {
|
|
2306
2373
|
addServerHandler({ route: AGENT_SKILLS_INDEX_ROUTE, handler: resolve$1("./runtime/server/routes/agent-skills-index"), lazy: true });
|
|
@@ -2376,7 +2443,7 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2376
2443
|
}
|
|
2377
2444
|
if (prerenderEnabled) {
|
|
2378
2445
|
const { setupPrerenderHandler } = await import('../chunks/prerender.mjs');
|
|
2379
|
-
setupPrerenderHandler(config, buildDbPath, {
|
|
2446
|
+
setupPrerenderHandler({ ...config, llmsTxt: generateLlmsTxt ? config.llmsTxt : false }, buildDbPath, {
|
|
2380
2447
|
name: siteConfig.name,
|
|
2381
2448
|
url: siteConfig.url ? withSiteUrl("/", { withBase: true }) : void 0,
|
|
2382
2449
|
description: siteConfig.description
|
|
@@ -2399,7 +2466,7 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2399
2466
|
icon: "carbon:ai-label"
|
|
2400
2467
|
}, resolve$1, nuxt);
|
|
2401
2468
|
}
|
|
2402
|
-
for (const route of ["/llms.txt", "/llms-full.txt"]) {
|
|
2469
|
+
for (const route of [generateLlmsTxt && "/llms.txt", generateLlmsTxt && "/llms/**", config.llmsFullTxt !== false && "/llms-full.txt"].filter((route2) => Boolean(route2))) {
|
|
2403
2470
|
extendRouteRules(route, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
2404
2471
|
}
|
|
2405
2472
|
if (config.sitemapMd !== false) {
|
|
@@ -2446,13 +2513,12 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2446
2513
|
const assets = wrangler.assets ||= {};
|
|
2447
2514
|
const fileRouting = await readCloudflareAssetRouting(nitro.options.rootDir);
|
|
2448
2515
|
const routing = defu$1({ value: assets.run_worker_first }, { value: fileRouting.root }).value;
|
|
2449
|
-
|
|
2516
|
+
const additions = cloudflareWorkerFirstAdditions(routing, pages);
|
|
2450
2517
|
const environmentConfig = wrangler;
|
|
2451
2518
|
const configuredEnvironments = Object.values(environmentConfig.env || {}).flatMap((env) => env.assets?.run_worker_first === void 0 ? [] : [env.assets.run_worker_first]);
|
|
2452
2519
|
const fallbackPaths = cloudflareStaticFallbackPaths(routing, pages, [...fileRouting.environments, ...configuredEnvironments]);
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
assets.run_worker_first = [...Array.isArray(assets.run_worker_first) ? assets.run_worker_first : [], ...addedPaths];
|
|
2520
|
+
if (additions.length > 0)
|
|
2521
|
+
assets.run_worker_first = [...Array.isArray(assets.run_worker_first) ? assets.run_worker_first : [], ...additions];
|
|
2456
2522
|
nitro.options.runtimeConfig["nuxt-ai-ready"].cloudflarePrerenderedPages = fallbackPaths;
|
|
2457
2523
|
}
|
|
2458
2524
|
const plan = planStaticMarkdownHeaderRules(
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ A page visit in dev or production does not index anything.
|
|
|
31
31
|
|
|
32
32
|
With no `aiReady` config, the module adds:
|
|
33
33
|
|
|
34
|
-
- `/llms.txt`: site name, description, resource links, and
|
|
34
|
+
- `/llms.txt`: site name, description, resource links, and current sitemap pages. Stored pages supply metadata. Large lists split under `/llms/`.
|
|
35
35
|
- `/llms-full.txt`: the full Markdown of each prerendered page. Only prerendered pages go in the static file.
|
|
36
36
|
- A `.md` twin per page. `/about` becomes `/about.md`, and `/` becomes `/index.md`. Paths under `/api` and `/_` get none.
|
|
37
37
|
- Accept negotiation: a request that prefers `text/markdown` gets a `307` to the `.md` URL. Responses send `Vary: Accept`. AI bot user agents get Markdown only with `botNegotiation: true`.
|
|
@@ -58,6 +58,32 @@ export default defineNuxtConfig({
|
|
|
58
58
|
})
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Publish separate discovery files from named sitemaps. Use module configuration before writing sitemap and database middleware:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
export default defineNuxtConfig({
|
|
65
|
+
aiReady: {
|
|
66
|
+
llmsFullTxt: false,
|
|
67
|
+
llmsTxt: {
|
|
68
|
+
markdownLinks: true,
|
|
69
|
+
sitemaps: ['pages', 'skills', 'demos'],
|
|
70
|
+
sections: [
|
|
71
|
+
{ title: 'Skills', sitemap: 'skills', path: '/llms/skills.txt' },
|
|
72
|
+
{ title: 'Demos', sitemap: 'demos', path: '/llms/demos.txt' },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The root links each section file. Chunked sitemaps keep their configured name.
|
|
80
|
+
Every file stays within `maxLength`, default 80,000 characters. Static builds emit every linked child.
|
|
81
|
+
GET and HEAD work on every generated discovery file. Empty sections produce no link or file.
|
|
82
|
+
|
|
83
|
+
For manual discovery, place `public/llms.txt`. The module preserves it and keeps Markdown conversion enabled.
|
|
84
|
+
If a server handler owns discovery, set `llmsTxt: false`.
|
|
85
|
+
Use `llmsFullTxt: false` instead of redirecting the full export to the index.
|
|
86
|
+
|
|
61
87
|
Remove an element from the Markdown on every path, build and runtime. Put it in `mdreamOptions`; the defaults `minimal` and `clean` stay on:
|
|
62
88
|
|
|
63
89
|
```ts
|
|
@@ -92,9 +118,12 @@ Runtime indexing, database drivers, Cloudflare D1, and the server query helpers:
|
|
|
92
118
|
|
|
93
119
|
## Traps
|
|
94
120
|
|
|
121
|
+
- **Discovery is not authorization.** Selected sitemaps exclude stale rows from `llms.txt`; they do not delete stored content or protect other APIs.
|
|
122
|
+
- **Custom route policies must allow Markdown twins.** If middleware rejects `/skills/planning.md`, allow it whenever `/skills/planning` is allowed.
|
|
95
123
|
- **The same edit in both hooks applies twice.** During prerendering the Nitro `ai-ready:page:markdown` hook runs, then the Nuxt hook of the same name. Put an edit in one of them, not both.
|
|
96
124
|
- **Content source pages skip conversion hooks.** A Nuxt Content page ignores `mdreamOptions` and both Nitro hooks.
|
|
97
125
|
- **Dev has no page data.** Query helpers, MCP tools, and search return empty results in `nuxt dev`. Test with `nuxt build` and `node .output/server/index.mjs`.
|
|
126
|
+
- **Cloudflare static pages negotiate only with `contentNegotiation: true`.** The default leaves prerendered HTML on the asset service, so `Accept: text/markdown` gets HTML there. With `true`, prerendered pages go into `assets.run_worker_first`: exact paths up to Cloudflare's 100 entries, directory globs such as `/docs/*` past that. More than 100 top-level pages fail the build; set broader patterns yourself.
|
|
98
127
|
- **Response caching turns negotiation off.** An ISR, `swr`, or `cache` route rule without `varies: ['accept']` serves HTML to Markdown clients. With `botNegotiation: true`, the rule needs `varies: ['accept', 'sec-fetch-dest', 'user-agent']`. The `.md` URL still works. For a CDN that caches by URL only, set `contentNegotiation: false`.
|
|
99
128
|
- **Emptying `ctx.markdown` does not hide a page.** It drops the body from `llms-full.txt`. The URL stays in `llms.txt` and in the index. Use `sitemap.exclude` for discovery, and access control for private pages.
|
|
100
129
|
- **Root `skills/` folders require explicit publication.** Automatic discovery scans public directories only. Set `agentSkills.dir: 'skills'` to publish root skills intentionally.
|
|
@@ -125,7 +154,13 @@ Runtime indexing, database drivers, Cloudflare D1, and the server query helpers:
|
|
|
125
154
|
|
|
126
155
|
## Config
|
|
127
156
|
|
|
128
|
-
- `llmsTxt
|
|
157
|
+
- `llmsTxt` (`object`): generated discovery. Set `false` for application-owned server routes. A public `llms.txt` disables generation automatically.
|
|
158
|
+
- `llmsTxt.pages` (`'sitemap'`): sitemap admission. Use `'all'` for stored pages outside the sitemap, or `false` for configured sections only.
|
|
159
|
+
- `llmsTxt.sitemaps`: selected sitemap names. An empty array stops sitemap discovery.
|
|
160
|
+
- `llmsTxt.maxLength` (`80000`): maximum characters per file. Minimum `1024`. Larger lists split without dropping links.
|
|
161
|
+
- `llmsFullTxt` (`true`): generate the full export and advertise its link. Set `false` to omit both.
|
|
162
|
+
- `llmsTxt.directive` (`{ markdown: true, html: false }`): a `> Index of every page on this site: [llms.txt](...)` line after the frontmatter of each page's `.md`; `html: true` adds a visually hidden `/llms.txt` link at the top of each HTML body.
|
|
163
|
+
- `llmsTxt.markdownLinks` (`true`): link page entries in `llms.txt` to their `.md` twins. `false` links canonical URLs. With `site.url` set, page links in `llms.txt`, `sitemap.md`, and `.md` bodies are absolute.
|
|
129
164
|
- `describedby` (`true`): the `describedby` link tag and header.
|
|
130
165
|
- `tools.<listPages|searchPages|getPageMarkdown>.<mcp|webmcp>.enabled`: attach a built in tool to one transport only.
|
|
131
166
|
- `autoI18n` (`true`): with `@nuxtjs/i18n`, adds hreflang `Link` headers and a languages section in `llms.txt`.
|