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.
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { readFile, mkdir, writeFile, access } from 'node:fs/promises';
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/read-page-data.mjs' {
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[]
@@ -1840,7 +1845,7 @@ ${details}`);
1840
1845
  const llmsFullRoute = withSiteUrl("llms-full.txt", { withBase: true });
1841
1846
  defaultLlmsTxtSections.push({
1842
1847
  title: "LLM Resources",
1843
- links: [
1848
+ links: config.llmsFullTxt === false ? [] : [
1844
1849
  {
1845
1850
  title: "Full Content",
1846
1851
  href: llmsFullRoute,
@@ -1853,18 +1858,31 @@ ${details}`);
1853
1858
  defaultLlmsTxtSections.push({
1854
1859
  title: "Agent Skills",
1855
1860
  description: `Skills an agent can install from this site. The index at ${indexUrl} carries a sha256 digest for each one.`,
1856
- 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) => ({
1857
1862
  title: link.name,
1858
1863
  href: link.source === "local" ? withSiteUrl(link.href.slice(1), { withBase: true }) : resolveExternalSkillUrl(link.href, indexUrl),
1859
1864
  description: link.description
1860
- }))
1865
+ }))]
1861
1866
  });
1862
1867
  }
1868
+ const directiveConfig = config.llmsTxt === false ? void 0 : config.llmsTxt?.directive;
1863
1869
  const llmsTxtDirective = {
1864
- markdown: config.llmsTxt?.directive?.markdown !== false,
1865
- html: config.llmsTxt?.directive?.html === true
1870
+ markdown: directiveConfig?.markdown !== false,
1871
+ html: directiveConfig?.html === true
1866
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);
1867
1884
  const mergedLlmsTxt = config.llmsTxt ? {
1885
+ ...parsedLlmsTxt.config,
1868
1886
  markdownLinks: config.llmsTxt.markdownLinks ?? true,
1869
1887
  directive: llmsTxtDirective,
1870
1888
  sections: [
@@ -1880,6 +1898,9 @@ ${details}`);
1880
1898
  await nuxt.callHook("ai-ready:llms-txt", llmsTxtPayload);
1881
1899
  mergedLlmsTxt.sections = llmsTxtPayload.sections;
1882
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}`);
1883
1904
  const prerenderCacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt-seo/ai-ready/routes");
1884
1905
  const buildDbPath = join(nuxt.options.buildDir, ".data/ai-ready/build.db");
1885
1906
  const runtimeSyncConfig = typeof config.runtimeSync === "object" ? config.runtimeSync : {};
@@ -1896,6 +1917,8 @@ ${details}`);
1896
1917
  if (apiCatalogRegistered)
1897
1918
  return;
1898
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." });
1899
1922
  addServerHandler({ route: API_CATALOG_PATH, handler: resolve$1("./runtime/server/routes/api-catalog"), lazy: true });
1900
1923
  const apiCatalogLink = `<${catalog.href}>; rel="api-catalog"`;
1901
1924
  extendRouteRules("/**", {
@@ -2273,6 +2296,7 @@ export function trackDrizzleWork(event, work) { return work }
2273
2296
  cron: !!config.cron
2274
2297
  })}`;
2275
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'`;
2276
2300
  });
2277
2301
  nuxt.options.runtimeConfig["nuxt-ai-ready"] = {
2278
2302
  version: version || "0.0.0",
@@ -2288,6 +2312,7 @@ export function trackDrizzleWork(event, work) { return work }
2288
2312
  swr: true
2289
2313
  }),
2290
2314
  llmsTxt: mergedLlmsTxt,
2315
+ llmsFullTxt: config.llmsFullTxt !== false,
2291
2316
  llmsTxtCacheSeconds: config.llmsTxtCacheSeconds ?? 600,
2292
2317
  prerenderCacheDir,
2293
2318
  database,
@@ -2335,10 +2360,14 @@ export function trackDrizzleWork(event, work) { return work }
2335
2360
  mode: "server",
2336
2361
  src: resolve$1("./runtime/app/plugins/md-alternate.server")
2337
2362
  });
2338
- addServerHandler({ route: "/llms.txt", handler: resolve$1("./runtime/server/routes/llms.txt.get"), lazy: true });
2339
- addServerHandler({ route: "/llms-full.txt", handler: resolve$1("./runtime/server/routes/llms-full.txt.get"), lazy: true });
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 });
2340
2369
  if (config.sitemapMd !== false) {
2341
- 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 });
2342
2371
  }
2343
2372
  if (agentSkillsResult._tag === "Enabled") {
2344
2373
  addServerHandler({ route: AGENT_SKILLS_INDEX_ROUTE, handler: resolve$1("./runtime/server/routes/agent-skills-index"), lazy: true });
@@ -2414,7 +2443,7 @@ export function trackDrizzleWork(event, work) { return work }
2414
2443
  }
2415
2444
  if (prerenderEnabled) {
2416
2445
  const { setupPrerenderHandler } = await import('../chunks/prerender.mjs');
2417
- setupPrerenderHandler(config, buildDbPath, {
2446
+ setupPrerenderHandler({ ...config, llmsTxt: generateLlmsTxt ? config.llmsTxt : false }, buildDbPath, {
2418
2447
  name: siteConfig.name,
2419
2448
  url: siteConfig.url ? withSiteUrl("/", { withBase: true }) : void 0,
2420
2449
  description: siteConfig.description
@@ -2437,7 +2466,7 @@ export function trackDrizzleWork(event, work) { return work }
2437
2466
  icon: "carbon:ai-label"
2438
2467
  }, resolve$1, nuxt);
2439
2468
  }
2440
- 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))) {
2441
2470
  extendRouteRules(route, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
2442
2471
  }
2443
2472
  if (config.sitemapMd !== false) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "3.2.0",
4
+ "version": "3.3.0",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -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 a `## Pages` list. In dev it lists sitemap routes without titles.
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,6 +118,8 @@ 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,6 +154,11 @@ Runtime indexing, database drivers, Cloudflare D1, and the server query helpers:
126
154
 
127
155
  ## Config
128
156
 
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.
129
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.
130
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.
131
164
  - `describedby` (`true`): the `describedby` link tag and header.