nuxt-ai-ready 1.5.7 → 1.5.8

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/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "1.5.7",
7
+ "version": "1.5.8",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { mkdir, writeFile, appendFile, stat, access, readFile } from 'node:fs/promises';
3
- import { join, dirname } from 'node:path';
4
- import { useLogger, useNuxt, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, hasNuxtModule, addServerPlugin, addServerHandler, addPlugin } from '@nuxt/kit';
2
+ import { mkdir, writeFile, appendFile, stat, readdir, access, readFile } from 'node:fs/promises';
3
+ import { join, dirname, relative, resolve } from 'node:path';
4
+ import { useLogger, useNuxt, resolveFiles, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, hasNuxtModule, addServerPlugin, addServerHandler, addPlugin } from '@nuxt/kit';
5
5
  import defu from 'defu';
6
6
  import { installNuxtSiteConfig, useSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
7
7
  import { setupDevToolsUI } from 'nuxtseo-shared/devtools';
@@ -9,7 +9,7 @@ import { resolveNuxtContentVersion } from 'nuxtseo-shared/kit';
9
9
  import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
10
10
  import { parseSitemapXml } from '@nuxtjs/sitemap/utils';
11
11
  import { colorize } from 'consola/utils';
12
- import { withLeadingSlash, withBase } from 'ufo';
12
+ import { withLeadingSlash, withBase, joinURL } from 'ufo';
13
13
  import { normalizePagePath, toMarkdownPath } from '../dist/runtime/markdown-path.js';
14
14
  import { toLogicalRoute, toDeployedRoute } from '../dist/runtime/route-path.js';
15
15
  import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../dist/runtime/server/db/shared.js';
@@ -25,6 +25,41 @@ const PRERENDER_PAGE_TIMEOUT = 3e4;
25
25
  const RE_HTML_MD_EXT = /\.(html|md)$/;
26
26
  const RE_INDEX_SUFFIX = /\/index$/;
27
27
  const RE_MD_EXT = /\.md$/;
28
+ const RE_NEGATED_GLOB = /^(!?)(.*)$/;
29
+ const RE_PARENT_DIR_GLOB = /!?\.\.\//;
30
+ const MARKDOWN_LINK_AVAILABILITY_FILE = "markdown-link-availability.json";
31
+ async function findOutputMarkdownPaths(publicDir, baseURL, srcDir, ignoredPaths, publicAssets) {
32
+ const paths = /* @__PURE__ */ new Set();
33
+ async function visit(directory, urlBase, relativeDirectory = "") {
34
+ const entries = await readdir(directory, { withFileTypes: true });
35
+ await Promise.all(entries.map(async (entry) => {
36
+ const relativePath = join(relativeDirectory, entry.name);
37
+ if (entry.isDirectory()) {
38
+ await visit(join(directory, entry.name), urlBase, relativePath);
39
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
40
+ const urlPath = joinURL(urlBase, relativePath.replaceAll("\\", "/"));
41
+ paths.add(joinURL(baseURL, urlPath));
42
+ }
43
+ }));
44
+ }
45
+ await visit(publicDir, "/");
46
+ for (const asset of publicAssets) {
47
+ const patterns = [
48
+ "**/*.md",
49
+ ...ignoredPaths.map((ignoredPath) => {
50
+ const [, negated = "", pattern = ""] = ignoredPath.match(RE_NEGATED_GLOB) || [];
51
+ const assetPattern = pattern.startsWith("*") ? pattern : relative(asset.dir, resolve(srcDir, pattern)).replaceAll("\\", "/");
52
+ return `${negated ? "" : "!"}${assetPattern}`;
53
+ }).filter((pattern) => !RE_PARENT_DIR_GLOB.test(pattern))
54
+ ];
55
+ const files = await resolveFiles(asset.dir, patterns);
56
+ for (const file of files) {
57
+ const urlPath = joinURL(asset.baseURL || "/", relative(asset.dir, file).replaceAll("\\", "/"));
58
+ paths.add(joinURL(baseURL, urlPath));
59
+ }
60
+ }
61
+ return [...paths].sort();
62
+ }
28
63
  async function fetchPreviousMeta(siteUrl, indexNow) {
29
64
  const metaUrl = withBase("/__ai-ready/pages.meta.json", siteUrl);
30
65
  logger.info(`Fetching previous build meta from ${metaUrl}`);
@@ -399,6 +434,20 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, indexNo
399
434
  await submitIndexNow(changed, added, state.siteInfo.url, state.indexNow);
400
435
  }
401
436
  }
437
+ if (state.dbPath && state.llmsTxtConfig?.markdownLinks) {
438
+ const availabilityFile = join(dirname(state.dbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
439
+ const paths = await findOutputMarkdownPaths(
440
+ nitro.options.output.publicDir,
441
+ nitro.options.baseURL,
442
+ nitro.options.srcDir,
443
+ nitro.options.ignore,
444
+ nitro.options.publicAssets
445
+ );
446
+ await writeFile(availabilityFile, JSON.stringify({
447
+ runtimeMarkdownAvailable: !nitro.options.static,
448
+ paths
449
+ }), "utf-8");
450
+ }
402
451
  const llmsStats = await prerenderRoute(nitro, "/llms.txt");
403
452
  const llmsFullStats = await stat(state.llmsFullTxtPath);
404
453
  nitro._prerenderedRoutes.push({ route: "/llms-full.txt", fileName: "/llms-full.txt" });
@@ -464,6 +513,10 @@ export {}
464
513
  addTemplate({
465
514
  filename: "types/ai-ready-virtual.d.ts",
466
515
  getContents: () => `declare module '#ai-ready-virtual/read-page-data.mjs' {
516
+ export function readMarkdownLinkAvailabilityFromFilesystem(): Promise<{
517
+ runtimeMarkdownAvailable: boolean
518
+ paths: string[]
519
+ }>
467
520
  export function readPageDataFromFilesystem(): Promise<{
468
521
  pages: Array<{
469
522
  route: string
@@ -765,12 +818,13 @@ const module$1 = defineNuxtModule({
765
818
  }
766
819
  }
767
820
  const mergedLlmsTxt = config.llmsTxt ? {
821
+ markdownLinks: config.llmsTxt.markdownLinks ?? false,
768
822
  sections: [
769
823
  ...defaultLlmsTxtSections,
770
824
  ...config.llmsTxt.sections || []
771
825
  ],
772
826
  notes: config.llmsTxt.notes
773
- } : { sections: defaultLlmsTxtSections };
827
+ } : { markdownLinks: false, sections: defaultLlmsTxtSections };
774
828
  const llmsTxtPayload = {
775
829
  sections: mergedLlmsTxt.sections || [],
776
830
  notes: typeof mergedLlmsTxt.notes === "string" ? [mergedLlmsTxt.notes] : mergedLlmsTxt.notes || []
@@ -859,7 +913,30 @@ const module$1 = defineNuxtModule({
859
913
  }
860
914
  }
861
915
  nitroConfig.virtual = nitroConfig.virtual || {};
862
- nitroConfig.virtual["#ai-ready-virtual/read-page-data.mjs"] = nuxt.options.dev ? `export async function readPageDataFromFilesystem() { return { pages: [], errorRoutes: [] } }` : `
916
+ const markdownLinkAvailabilityPath = join(dirname(buildDbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
917
+ nitroConfig.virtual["#ai-ready-virtual/read-page-data.mjs"] = nuxt.options.dev ? `
918
+ export async function readPageDataFromFilesystem() { return { pages: [], errorRoutes: [] } }
919
+ export async function readMarkdownLinkAvailabilityFromFilesystem() { return { runtimeMarkdownAvailable: false, paths: [] } }
920
+ ` : `
921
+ export async function readMarkdownLinkAvailabilityFromFilesystem() {
922
+ if (!import.meta.prerender) {
923
+ return { runtimeMarkdownAvailable: false, paths: [] }
924
+ }
925
+
926
+ const { readFile } = await import('node:fs/promises')
927
+ try {
928
+ const availability = JSON.parse(await readFile(${JSON.stringify(markdownLinkAvailabilityPath)}, 'utf8'))
929
+ return {
930
+ runtimeMarkdownAvailable: availability?.runtimeMarkdownAvailable === true,
931
+ paths: Array.isArray(availability?.paths) ? availability.paths.filter(path => typeof path === 'string') : [],
932
+ }
933
+ }
934
+ catch (error) {
935
+ console.warn('[nuxt-ai-ready] Failed to read Markdown link availability; keeping canonical links.', error)
936
+ return { runtimeMarkdownAvailable: false, paths: [] }
937
+ }
938
+ }
939
+
863
940
  export async function readPageDataFromFilesystem() {
864
941
  if (!import.meta.prerender) {
865
942
  return { pages: [], errorRoutes: [] }
@@ -1,13 +1,33 @@
1
1
  import { useRuntimeConfig } from "nitropack/runtime";
2
+ import { decodePath } from "ufo";
2
3
  import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
3
4
  import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
4
5
  import { formatAvailableLanguagesSection, formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
6
+ import { toMarkdownPath } from "./markdown-path.js";
5
7
  import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
6
8
  import { queryPages } from "./server/db/queries.js";
7
9
  import { logger } from "./server/logger.js";
8
10
  import { resolveLocaleFromRoute } from "./server/utils/i18n.js";
9
11
  import { fetchSitemapUrls } from "./server/utils/sitemap.js";
10
12
  export { normalizeLlmsTxtConfig };
13
+ function hasRuntimeMarkdownHandler(pathname) {
14
+ const lastSegment = pathname.split("/").pop() || "";
15
+ return !pathname.startsWith("/api") && !pathname.startsWith("/_") && !pathname.startsWith("/@") && !lastSegment.includes(".");
16
+ }
17
+ async function findMarkdownLinkAvailability() {
18
+ if (!import.meta.prerender) {
19
+ return {
20
+ runtimeMarkdownAvailable: true,
21
+ paths: /* @__PURE__ */ new Set()
22
+ };
23
+ }
24
+ const { readMarkdownLinkAvailabilityFromFilesystem } = await import("#ai-ready-virtual/read-page-data.mjs");
25
+ const availability = await readMarkdownLinkAvailabilityFromFilesystem();
26
+ return {
27
+ runtimeMarkdownAvailable: availability.runtimeMarkdownAvailable,
28
+ paths: new Set(availability.paths.map(decodePath))
29
+ };
30
+ }
11
31
  function getGroupPrefix(url, depth) {
12
32
  const segments = url.split("/").filter(Boolean);
13
33
  if (segments.length === 0)
@@ -177,8 +197,20 @@ Canonical Origin: ${canonicalSiteUrl}`);
177
197
  seenPaths.add(pathname);
178
198
  }
179
199
  }
200
+ let resolvePageHref = resolvePath;
201
+ if (llmsTxtConfig.markdownLinks) {
202
+ const markdownLinkAvailability = await findMarkdownLinkAvailability();
203
+ resolvePageHref = (pathname) => {
204
+ const deployedPathname = resolvePath(pathname);
205
+ const markdownPath = resolvePath(toMarkdownPath(pathname));
206
+ if (markdownLinkAvailability.paths.has(decodePath(markdownPath)) || markdownLinkAvailability.runtimeMarkdownAvailable && hasRuntimeMarkdownHandler(pathname)) {
207
+ return markdownPath;
208
+ }
209
+ return deployedPathname;
210
+ };
211
+ }
180
212
  for (const page of [...prerendered, ...other])
181
- page.href = resolvePath(page.pathname);
213
+ page.href = resolvePageHref(page.pathname);
182
214
  if (i18n) {
183
215
  const pageCounts = /* @__PURE__ */ new Map();
184
216
  for (const locale of i18n.locales) pageCounts.set(locale.code, 0);
@@ -186,7 +218,7 @@ Canonical Origin: ${canonicalSiteUrl}`);
186
218
  const code = p.locale || resolveLocaleFromRoute(p.pathname, i18n).locale;
187
219
  pageCounts.set(code, (pageCounts.get(code) ?? 0) + 1);
188
220
  }
189
- parts.push(...formatAvailableLanguagesSection(i18n, pageCounts, resolvePath));
221
+ parts.push(...formatAvailableLanguagesSection(i18n, pageCounts, resolvePageHref));
190
222
  parts.push("");
191
223
  }
192
224
  const isDefaultLocale = (item) => {
@@ -271,6 +271,13 @@ export interface LlmsTxtSection {
271
271
  * Structured llms.txt configuration
272
272
  */
273
273
  export interface LlmsTxtConfig {
274
+ /**
275
+ * Rewrite automatically generated page links to their Markdown representation
276
+ * when the final static output contains the file or the deployment retains
277
+ * the runtime Markdown handler.
278
+ * @default false
279
+ */
280
+ markdownLinks?: boolean;
274
281
  /** The sections of the documentation */
275
282
  sections?: LlmsTxtSection[];
276
283
  /** Additional heading-free preamble notes rendered before file-list sections */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "1.5.7",
4
+ "version": "1.5.8",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",