nuxt-ai-ready 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { realpath, readFile, readdir } from 'node:fs/promises';
3
3
  import { join, resolve, isAbsolute, relative, sep } from 'node:path';
4
4
  import { parseDocument } from 'yaml';
5
- import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.Dmvy7CIJ.mjs';
5
+ import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.DH9mSayy.mjs';
6
6
  import 'node:module';
7
7
  import 'node:url';
8
8
  import '@nuxt/kit';
@@ -6,7 +6,9 @@ import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
6
6
  import { createPrerenderFetch } from 'nuxtseo-shared/prerender';
7
7
  import { collectSitemap } from 'sitemapd/parse';
8
8
  import { withLeadingSlash, joinURL, withBase } from 'ufo';
9
- import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.Dmvy7CIJ.mjs';
9
+ import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.DH9mSayy.mjs';
10
+ import { insertLlmsTxtDirective } from '../../dist/runtime/llms-txt-directive.js';
11
+ import { absoluteSiteUrl } from '../../dist/runtime/markdown-links.js';
10
12
  import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
11
13
  import { toDeployedRoute, toLogicalRoute } from '../../dist/runtime/route-path.js';
12
14
  import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
@@ -347,7 +349,8 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
347
349
  await initPromise;
348
350
  const parsed = JSON.parse(route.contents || "{}");
349
351
  await processMarkdownRoute(state, nuxt, pageRoute, parsed);
350
- route.contents = options.sitemapMd === false ? parsed.markdown : appendSitemapSection(parsed.markdown, toDeployedRoute(SITEMAP_MD_ROUTE, nitro.options.baseURL));
352
+ const withSitemap = options.sitemapMd === false ? parsed.markdown : appendSitemapSection(parsed.markdown, absoluteSiteUrl(toDeployedRoute(SITEMAP_MD_ROUTE, nitro.options.baseURL), siteInfo?.url));
353
+ route.contents = llmsTxtConfig?.directive?.markdown === false ? withSitemap : insertLlmsTxtDirective(withSitemap, absoluteSiteUrl(toDeployedRoute("/llms.txt", nitro.options.baseURL), siteInfo?.url));
351
354
  route.contentType = "text/markdown; charset=utf-8";
352
355
  state.totalProcessingTime += Date.now() - pageStartTime;
353
356
  });
@@ -409,10 +412,12 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
409
412
  }
410
413
  if (state.dbPath && state.llmsTxtConfig?.markdownLinks) {
411
414
  const availabilityFile = join(dirname(state.dbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
415
+ const options2 = nitro.options;
416
+ const ignoreBaseDir = options2.srcDir || (typeof options2.serverDir === "string" ? options2.serverDir : void 0) || options2.rootDir;
412
417
  const paths = await findOutputMarkdownPaths(
413
418
  nitro.options.output.publicDir,
414
419
  nitro.options.baseURL,
415
- nitro.options.srcDir,
420
+ ignoreBaseDir,
416
421
  nitro.options.ignore,
417
422
  nitro.options.publicAssets
418
423
  );
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": "^4.6.0 || ^5.0.0-0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "3.1.0",
7
+ "version": "3.2.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,7 +7,7 @@ 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.Dmvy7CIJ.mjs';
10
+ export { m as default } from './shared/nuxt-ai-ready.DH9mSayy.mjs';
11
11
  import './shared/nuxt-ai-ready.B3hAuOLQ.mjs';
12
12
  import '../dist/runtime/route-path.js';
13
13
  import '../dist/runtime/server/utils/sitemap-md.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
+ }
@@ -4,6 +4,7 @@ import { useRuntimeConfig } from "#nuxtseo/nitro";
4
4
  import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
5
5
  import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
6
6
  import { formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
7
+ import { absoluteSiteUrl } from "./markdown-links.js";
7
8
  import { hasMarkdownTwin, toMarkdownPath } from "./markdown-path.js";
8
9
  import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
9
10
  import { queryPages } from "./server/db/queries.js";
@@ -207,6 +208,8 @@ Canonical Origin: ${canonicalSiteUrl}`);
207
208
  return deployedPathname;
208
209
  };
209
210
  }
211
+ const resolvePagePath = resolvePageHref;
212
+ resolvePageHref = (pathname) => absoluteSiteUrl(resolvePagePath(pathname), canonicalSiteUrl);
210
213
  for (const page of [...prerendered, ...other])
211
214
  page.href = resolvePageHref(page.pathname);
212
215
  if (i18nRuntime._tag === "enabled") {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Resolve root-relative link and image destinations against a site origin.
3
+ *
4
+ * A Markdown page read on its own, by an agent or from `llms-full.txt`, has
5
+ * no page URL to resolve `/docs/api` against, so such a link loses its
6
+ * target. Fenced code and inline code spans keep their text.
7
+ */
8
+ export declare function absoluteMarkdownLinks(markdown: string, siteUrl: string): string;
9
+ /** A deployed path on the site's origin, or the path itself when no site URL is known. */
10
+ export declare function absoluteSiteUrl(path: string, siteUrl: string | undefined): string;
@@ -0,0 +1,52 @@
1
+ const RE_FENCE = /^ {0,3}(`{3,}|~{3,})/;
2
+ const RE_INLINE_ROOT_LINK = /\]\((<?)\/(?!\/)/g;
3
+ const RE_REFERENCE_ROOT_LINK = /^( {0,3}\[[^\]]+\]:[\t ]*<?)\/(?!\/)/;
4
+ function closesFence(line, fence) {
5
+ const trimmed = line.trim();
6
+ return trimmed.length >= fence.length && trimmed[0] === fence[0] && [...trimmed].every((char) => char === fence[0]);
7
+ }
8
+ function backtickRun(line, start) {
9
+ let end = start;
10
+ while (line[end] === "`")
11
+ end++;
12
+ return end - start;
13
+ }
14
+ function rewriteOutsideCodeSpans(line, rewrite) {
15
+ let output = "";
16
+ let last = 0;
17
+ let index = line.indexOf("`");
18
+ while (index !== -1) {
19
+ const length = backtickRun(line, index);
20
+ let close = line.indexOf("`", index + length);
21
+ while (close !== -1 && backtickRun(line, close) !== length)
22
+ close = line.indexOf("`", close + backtickRun(line, close));
23
+ if (close === -1) {
24
+ index = line.indexOf("`", index + length);
25
+ continue;
26
+ }
27
+ output += rewrite(line.slice(last, index)) + line.slice(index, close + length);
28
+ last = close + length;
29
+ index = line.indexOf("`", last);
30
+ }
31
+ return output + rewrite(line.slice(last));
32
+ }
33
+ export function absoluteMarkdownLinks(markdown, siteUrl) {
34
+ const origin = new URL(siteUrl).origin;
35
+ let fence;
36
+ return markdown.split(/(?<=\n)/).map((line) => {
37
+ if (fence) {
38
+ if (closesFence(line, fence))
39
+ fence = void 0;
40
+ return line;
41
+ }
42
+ const opening = line.match(RE_FENCE);
43
+ if (opening) {
44
+ fence = opening[1];
45
+ return line;
46
+ }
47
+ return rewriteOutsideCodeSpans(line, (text) => text.replace(RE_REFERENCE_ROOT_LINK, `$1${origin}/`).replace(RE_INLINE_ROOT_LINK, `]($1${origin}/`));
48
+ }).join("");
49
+ }
50
+ export function absoluteSiteUrl(path, siteUrl) {
51
+ return siteUrl ? new URL(path, new URL(siteUrl).origin).href : path;
52
+ }
@@ -1,6 +1,8 @@
1
1
  import { createError, defineEventHandler, getRequestURL, setHeader, setResponseStatus } from "#nuxtseo/h3";
2
2
  import { useNitroApp, useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { resolveLocaleAlternateUrl } from "../../i18n-url.js";
4
+ import { insertLlmsTxtDirective } from "../../llms-txt-directive.js";
5
+ import { absoluteMarkdownLinks } from "../../markdown-links.js";
4
6
  import { logger } from "../logger.js";
5
7
  import { computeLocaleAlternates, resolveLocaleFromRoute } from "../utils/i18n.js";
6
8
  import { resolveMarkdownRedirect } from "../utils/markdown-redirect.js";
@@ -49,7 +51,12 @@ export default defineEventHandler(async (event) => {
49
51
  const ctx = buildNegotiationContext(event, decision.path);
50
52
  const { path, config, resolvePath, resolveUrl, routeContext } = ctx;
51
53
  const canonicalUrl = resolveUrl(path);
52
- const finalizeMarkdown = (markdown) => config.sitemapMd === false ? markdown : appendSitemapSection(markdown, resolvePath(SITEMAP_MD_ROUTE));
54
+ const finalizeMarkdown = (markdown) => {
55
+ const withSitemap = config.sitemapMd === false ? markdown : appendSitemapSection(markdown, resolvePath(SITEMAP_MD_ROUTE));
56
+ const llmsTxt = config.llmsTxt;
57
+ const withDirective = llmsTxt?.directive?.markdown === false ? withSitemap : insertLlmsTxtDirective(withSitemap, resolveUrl("/llms.txt"));
58
+ return absoluteMarkdownLinks(withDirective, canonicalUrl);
59
+ };
53
60
  const [
54
61
  { tryGetContentMarkdown },
55
62
  { fetchRawWithEvent },
@@ -1,6 +1,7 @@
1
1
  import { createError, defineEventHandler } from "#nuxtseo/h3";
2
2
  import { useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { withSiteUrl } from "#site-config/server/composables/utils";
4
+ import { absoluteMarkdownLinks } from "../../markdown-links.js";
4
5
  import { toDeployedRoute } from "../../route-path.js";
5
6
  import { logger } from "../logger.js";
6
7
  import { convertHtmlToMarkdown, extractLastUpdated } from "../utils.js";
@@ -52,7 +53,7 @@ export default defineEventHandler(async (event) => {
52
53
  last_updated: contentPage.updatedAt
53
54
  });
54
55
  const markdown = `${frontmatter}
55
- ${contentPage.markdown}`;
56
+ ${absoluteMarkdownLinks(contentPage.markdown, canonicalUrl)}`;
56
57
  const headings = extractHeadingsFromMarkdown(contentPage.markdown);
57
58
  const keywords2 = extractKeywords(contentPage.markdown, "");
58
59
  return JSON.stringify({
@@ -0,0 +1,3 @@
1
+ /** Registered only with `llmsTxt.directive.html: true`. */
2
+ declare const _default: any;
3
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import { defineNitroPlugin, useRuntimeConfig } from "#nuxtseo/nitro";
2
+ import { llmsTxtDirectiveHtml } from "../../llms-txt-directive.js";
3
+ import { toDeployedRoute } from "../../route-path.js";
4
+ export default defineNitroPlugin((nitroApp) => {
5
+ nitroApp.hooks.hook("render:html", (html, { event }) => {
6
+ html.bodyPrepend.unshift(llmsTxtDirectiveHtml(toDeployedRoute("/llms.txt", useRuntimeConfig(event).app.baseURL)));
7
+ });
8
+ });
@@ -2,6 +2,7 @@ import { eventHandler, setHeader } from "#nuxtseo/h3";
2
2
  import { defineCachedFunction, useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { getSiteConfig } from "#site-config/server/composables";
4
4
  import { publicCacheControl } from "../../cache-control.js";
5
+ import { absoluteSiteUrl } from "../../markdown-links.js";
5
6
  import { toMarkdownPath } from "../../markdown-path.js";
6
7
  import { toDeployedRoute } from "../../route-path.js";
7
8
  import { queryPages } from "../db/queries.js";
@@ -17,11 +18,12 @@ async function buildSitemapMarkdown(event) {
17
18
  );
18
19
  }
19
20
  const baseURL = useRuntimeConfig(event).app.baseURL;
21
+ const siteConfig = getSiteConfig(event);
20
22
  return buildSitemapMd(
21
23
  pages.map((page) => ({ route: page.route, title: page.title, updatedAt: page.updatedAt })),
22
24
  {
23
- siteName: getSiteConfig(event).name,
24
- resolveHref: (route) => toDeployedRoute(toMarkdownPath(route), baseURL)
25
+ siteName: siteConfig.name,
26
+ resolveHref: (route) => absoluteSiteUrl(toDeployedRoute(toMarkdownPath(route), baseURL), siteConfig.url)
25
27
  }
26
28
  );
27
29
  }
@@ -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,
@@ -529,9 +529,25 @@ export interface LlmsTxtConfig {
529
529
  * Rewrite automatically generated page links to their Markdown representation
530
530
  * when the final static output contains the file or the deployment retains
531
531
  * the runtime Markdown handler.
532
- * @default false
532
+ * @default true
533
533
  */
534
534
  markdownLinks?: boolean;
535
+ /**
536
+ * Point each page at llms.txt, so an agent that lands on one page finds the
537
+ * whole site's index.
538
+ */
539
+ directive?: {
540
+ /**
541
+ * Add a blockquote line after the frontmatter of every page's `.md` response.
542
+ * @default true
543
+ */
544
+ markdown?: boolean;
545
+ /**
546
+ * Add a visually hidden link to `/llms.txt` at the top of every HTML page's body.
547
+ * @default false
548
+ */
549
+ html?: boolean;
550
+ };
535
551
  /** The sections of the documentation */
536
552
  sections?: LlmsTxtSection[];
537
553
  /** Additional heading-free preamble notes rendered before file-list sections */
@@ -666,18 +666,42 @@ function cloudflareStaticFallbackPaths(existing, pages, environments = []) {
666
666
  const patterns = [...existing || [], ...environments.flatMap((routing) => Array.isArray(routing) ? routing : [])];
667
667
  return pagePaths(pages).filter((path) => !patterns.some((pattern) => matchesPattern(path, pattern.startsWith("!") ? pattern.slice(1) : pattern)));
668
668
  }
669
- function cloudflareNegotiationPatterns(existing, pages) {
670
- if (existing === true || existing === false || pages.length === 0)
671
- return existing;
672
- const patterns = [...existing || []];
673
- for (const path of cloudflareStaticFallbackPaths(existing, pages)) {
674
- if (!patterns.includes(path))
675
- patterns.push(path);
669
+ function parentDirectories(path) {
670
+ const segments = path.split("/").filter(Boolean);
671
+ return segments.slice(0, -1).map((_, index) => `/${segments.slice(0, index + 1).join("/")}/`);
672
+ }
673
+ function fitWorkerFirstBudget(paths, budget) {
674
+ let remaining = [...paths];
675
+ const globs = [];
676
+ while (remaining.length + globs.length > budget) {
677
+ const counts = /* @__PURE__ */ new Map();
678
+ for (const path of remaining) {
679
+ for (const directory2 of parentDirectories(path))
680
+ counts.set(directory2, (counts.get(directory2) ?? 0) + 1);
681
+ }
682
+ let best;
683
+ for (const [directory2, count] of counts) {
684
+ if (!best || count > best[1] || count === best[1] && directory2.length > best[0].length)
685
+ best = [directory2, count];
686
+ }
687
+ if (!best || best[1] < 2)
688
+ return void 0;
689
+ const [directory] = best;
690
+ globs.push(`${directory}*`);
691
+ remaining = remaining.filter((path) => !path.startsWith(directory) || path === directory);
676
692
  }
677
- if (patterns.length > MAX_WORKER_FIRST_PATTERNS) {
678
- throw new Error("Cloudflare Markdown negotiation exceeds 100 worker-first patterns. Configure broader assets.run_worker_first patterns, or disable aiReady.contentNegotiation.");
693
+ return [...remaining, ...globs];
694
+ }
695
+ function cloudflareWorkerFirstAdditions(existing, pages) {
696
+ if (existing === true || existing === false || pages.length === 0)
697
+ return [];
698
+ const patterns = existing || [];
699
+ const paths = cloudflareStaticFallbackPaths(existing, pages).filter((path) => !patterns.includes(path));
700
+ const additions = fitWorkerFirstBudget(paths, MAX_WORKER_FIRST_PATTERNS - patterns.length);
701
+ if (!additions) {
702
+ 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
703
  }
680
- return patterns;
704
+ return additions;
681
705
  }
682
706
 
683
707
  const CACHE_GUIDE = "https://nuxtseo.com/docs/ai-ready/guides/markdown#cache-safety";
@@ -1219,6 +1243,14 @@ function createMcpServerCardEtag(card) {
1219
1243
  return `"${createHash("sha256").update(JSON.stringify(card)).digest("hex")}"`;
1220
1244
  }
1221
1245
 
1246
+ function opaqueJsonModule(exports) {
1247
+ const encode = (value) => JSON.stringify(Buffer.from(JSON.stringify(value), "utf8").toString("base64"));
1248
+ return [
1249
+ "const decode = value => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value), char => char.charCodeAt(0))))",
1250
+ ...Object.entries(exports).map(([name, value]) => `export const ${name} = decode(${encode(value)})`)
1251
+ ].join("\n");
1252
+ }
1253
+
1222
1254
  function escapeRegExp(value) {
1223
1255
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1224
1256
  }
@@ -1828,14 +1860,19 @@ ${details}`);
1828
1860
  }))
1829
1861
  });
1830
1862
  }
1863
+ const llmsTxtDirective = {
1864
+ markdown: config.llmsTxt?.directive?.markdown !== false,
1865
+ html: config.llmsTxt?.directive?.html === true
1866
+ };
1831
1867
  const mergedLlmsTxt = config.llmsTxt ? {
1832
- markdownLinks: config.llmsTxt.markdownLinks ?? false,
1868
+ markdownLinks: config.llmsTxt.markdownLinks ?? true,
1869
+ directive: llmsTxtDirective,
1833
1870
  sections: [
1834
1871
  ...defaultLlmsTxtSections,
1835
1872
  ...config.llmsTxt.sections || []
1836
1873
  ],
1837
1874
  notes: config.llmsTxt.notes
1838
- } : { markdownLinks: false, sections: defaultLlmsTxtSections };
1875
+ } : { markdownLinks: true, directive: llmsTxtDirective, sections: defaultLlmsTxtSections };
1839
1876
  const llmsTxtPayload = {
1840
1877
  sections: mergedLlmsTxt.sections || [],
1841
1878
  notes: typeof mergedLlmsTxt.notes === "string" ? [mergedLlmsTxt.notes] : mergedLlmsTxt.notes || []
@@ -2160,8 +2197,7 @@ export { unavailable as computeLocaleAlternates, unavailable as localePath, unav
2160
2197
  `;
2161
2198
  };
2162
2199
  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" ? `export const agentSkillsIndex = ${JSON.stringify(agentSkillsResult.index)}
2164
- export const localAgentSkillArtifacts = ${JSON.stringify(agentSkillsResult.localArtifacts)}` : "export const agentSkillsIndex = null\nexport const localAgentSkillArtifacts = {}";
2200
+ 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
2201
  const markdownLinkAvailabilityPath = join(dirname(buildDbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
2166
2202
  nitroConfig.virtual["#ai-ready-virtual/read-page-data.mjs"] = createBuildPageDataVirtual({
2167
2203
  buildDbPath,
@@ -2293,6 +2329,8 @@ export function trackDrizzleWork(event, work) { return work }
2293
2329
  }
2294
2330
  addNitroPlugin(resolve$1(getNitroVersion(nuxt) === 3 ? "./runtime/server/plugins/link-header-nitro3" : "./runtime/server/plugins/link-header"));
2295
2331
  addNitroPlugin(resolve$1("./runtime/server/plugins/markdown-negotiation"));
2332
+ if (llmsTxtDirective.html)
2333
+ addNitroPlugin(resolve$1("./runtime/server/plugins/llms-txt-directive-html"));
2296
2334
  addPlugin({
2297
2335
  mode: "server",
2298
2336
  src: resolve$1("./runtime/app/plugins/md-alternate.server")
@@ -2446,13 +2484,12 @@ export function trackDrizzleWork(event, work) { return work }
2446
2484
  const assets = wrangler.assets ||= {};
2447
2485
  const fileRouting = await readCloudflareAssetRouting(nitro.options.rootDir);
2448
2486
  const routing = defu$1({ value: assets.run_worker_first }, { value: fileRouting.root }).value;
2449
- cloudflareNegotiationPatterns(routing, pages);
2487
+ const additions = cloudflareWorkerFirstAdditions(routing, pages);
2450
2488
  const environmentConfig = wrangler;
2451
2489
  const configuredEnvironments = Object.values(environmentConfig.env || {}).flatMap((env) => env.assets?.run_worker_first === void 0 ? [] : [env.assets.run_worker_first]);
2452
2490
  const fallbackPaths = cloudflareStaticFallbackPaths(routing, pages, [...fileRouting.environments, ...configuredEnvironments]);
2453
- const addedPaths = cloudflareStaticFallbackPaths(routing, pages);
2454
- if (addedPaths.length > 0)
2455
- assets.run_worker_first = [...Array.isArray(assets.run_worker_first) ? assets.run_worker_first : [], ...addedPaths];
2491
+ if (additions.length > 0)
2492
+ assets.run_worker_first = [...Array.isArray(assets.run_worker_first) ? assets.run_worker_first : [], ...additions];
2456
2493
  nitro.options.runtimeConfig["nuxt-ai-ready"].cloudflarePrerenderedPages = fallbackPaths;
2457
2494
  }
2458
2495
  const plan = planStaticMarkdownHeaderRules(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "3.1.0",
4
+ "version": "3.2.0",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -95,6 +95,7 @@ Runtime indexing, database drivers, Cloudflare D1, and the server query helpers:
95
95
  - **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
96
  - **Content source pages skip conversion hooks.** A Nuxt Content page ignores `mdreamOptions` and both Nitro hooks.
97
97
  - **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`.
98
+ - **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
99
  - **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
100
  - **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
101
  - **Root `skills/` folders require explicit publication.** Automatic discovery scans public directories only. Set `agentSkills.dir: 'skills'` to publish root skills intentionally.
@@ -125,7 +126,8 @@ Runtime indexing, database drivers, Cloudflare D1, and the server query helpers:
125
126
 
126
127
  ## Config
127
128
 
128
- - `llmsTxt.markdownLinks` (`false`): link page entries in `llms.txt` to their `.md` twins.
129
+ - `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
+ - `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
131
  - `describedby` (`true`): the `describedby` link tag and header.
130
132
  - `tools.<listPages|searchPages|getPageMarkdown>.<mcp|webmcp>.enabled`: attach a built in tool to one transport only.
131
133
  - `autoI18n` (`true`): with `@nuxtjs/i18n`, adds hreflang `Link` headers and a languages section in `llms.txt`.