nuxt-ai-ready 0.4.1 → 0.4.2

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": "0.4.1",
7
+ "version": "0.4.2",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -99,6 +99,7 @@ function normalizeLlmsTxtConfig(config) {
99
99
  function createCrawlerState(pageDataPath, llmsFullTxtPath, siteInfo, llmsTxtConfig) {
100
100
  return {
101
101
  prerenderedRoutes: /* @__PURE__ */ new Set(),
102
+ errorRoutes: /* @__PURE__ */ new Set(),
102
103
  totalProcessingTime: 0,
103
104
  initialized: false,
104
105
  jsonlInitialized: false,
@@ -221,7 +222,7 @@ async function crawlSitemapEntries(state, nuxt, nitro, entries) {
221
222
  const res = await globalThis.$fetch(mdUrl, {
222
223
  headers: { "x-nitro-prerender": mdRoute }
223
224
  }).catch((err) => {
224
- logger.debug(`Failed to fetch ${mdUrl}: ${err.message}`);
225
+ logger.debug(`Skipping ${route}: ${err.message}`);
225
226
  return null;
226
227
  });
227
228
  if (!res)
@@ -230,7 +231,7 @@ async function crawlSitemapEntries(state, nuxt, nitro, entries) {
230
231
  await processMarkdownRoute(state, nuxt, route, parsed, lastmod);
231
232
  crawled++;
232
233
  }
233
- logger.debug(`Sitemap crawl complete: ${crawled} crawled, ${skipped} skipped (already indexed)`);
234
+ logger.debug(`Sitemap crawl complete: ${crawled} crawled, ${skipped} skipped`);
234
235
  return crawled;
235
236
  }
236
237
  async function crawlSitemapContent(state, nuxt, nitro, sitemapContent) {
@@ -296,6 +297,12 @@ function setupPrerenderHandler(pageDataPath, siteInfo, llmsTxtConfig) {
296
297
  const state = createCrawlerState(pageDataPath, llmsFullTxtPath, siteInfo, llmsTxtConfig);
297
298
  let initPromise = null;
298
299
  nitro.hooks.hook("prerender:generate", async (route) => {
300
+ if (route.error) {
301
+ const pageRoute2 = route.route.replace(/\.(html|md)$/, "").replace(/\/index$/, "") || "/";
302
+ state.errorRoutes.add(pageRoute2);
303
+ logger.debug(`Detected error page: ${pageRoute2}`);
304
+ return;
305
+ }
299
306
  if (!route.fileName?.endsWith(".md"))
300
307
  return;
301
308
  let pageRoute = route.route.replace(/\.md$/, "");
@@ -341,6 +348,13 @@ function setupPrerenderHandler(pageDataPath, siteInfo, llmsTxtConfig) {
341
348
  state.totalProcessingTime += Date.now() - pageStartTime;
342
349
  });
343
350
  async function writeLlmsFiles() {
351
+ if (state.pageDataPath && state.errorRoutes.size > 0) {
352
+ for (const route of state.errorRoutes) {
353
+ await appendFile(state.pageDataPath, `${JSON.stringify({ route, _error: true })}
354
+ `, "utf-8");
355
+ }
356
+ logger.debug(`Wrote ${state.errorRoutes.size} error routes to page data`);
357
+ }
344
358
  const llmsStats = await prerenderRoute(nitro, "/llms.txt");
345
359
  const llmsFullStats = await stat(state.llmsFullTxtPath);
346
360
  const kb = (b) => (b / 1024).toFixed(1);
@@ -536,14 +550,18 @@ import { readFile } from 'node:fs/promises'
536
550
 
537
551
  export async function readPageDataFromFilesystem() {
538
552
  if (!import.meta.prerender) {
539
- return null
553
+ return { pages: [], errorRoutes: [] }
540
554
  }
541
555
  const data = await readFile(${JSON.stringify(pageDataPath)}, 'utf-8').catch(() => null)
542
- if (!data) return []
543
- return data.trim().split('\\n').filter(Boolean).map(line => JSON.parse(line))
556
+ if (!data) return { pages: [], errorRoutes: [] }
557
+ const entries = data.trim().split('\\n').filter(Boolean).map(line => JSON.parse(line))
558
+ const pages = entries.filter(e => !e._error)
559
+ const errorRoutes = entries.filter(e => e._error).map(e => e.route)
560
+ return { pages, errorRoutes }
544
561
  }
545
562
  `;
546
- nitroConfig.virtual["#ai-ready-virtual/page-data.mjs"] = `export const pages = []`;
563
+ nitroConfig.virtual["#ai-ready-virtual/page-data.mjs"] = `export const pages = []
564
+ export const errorRoutes = []`;
547
565
  });
548
566
  nuxt.options.runtimeConfig["nuxt-ai-ready"] = {
549
567
  version: version || "0.0.0",
@@ -1,7 +1,123 @@
1
1
  import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
- import { getPages } from "./server/utils/pageData.js";
3
+ import { getErrorRoutes, getPages } from "./server/utils/pageData.js";
4
4
  import { fetchSitemapUrls } from "./server/utils/sitemap.js";
5
+ function getGroupPrefix(url, depth) {
6
+ const segments = url.split("/").filter(Boolean);
7
+ if (segments.length === 0)
8
+ return "/";
9
+ if (depth === 1 || segments.length === 1)
10
+ return `/${segments[0]}`;
11
+ return `/${segments[0]}/${segments[1]}`;
12
+ }
13
+ function getPathSegments(pathname) {
14
+ return pathname.split("/").filter((s) => Boolean(s));
15
+ }
16
+ function sortPagesByPath(pages) {
17
+ const twoSegmentCount = /* @__PURE__ */ new Map();
18
+ for (const page of pages) {
19
+ const prefix = getGroupPrefix(page.pathname, 2);
20
+ twoSegmentCount.set(prefix, (twoSegmentCount.get(prefix) || 0) + 1);
21
+ }
22
+ const segmentHasNested = /* @__PURE__ */ new Map();
23
+ for (const page of pages) {
24
+ const segments = getPathSegments(page.pathname);
25
+ const firstSegment = segments[0] || "";
26
+ if (!segmentHasNested.has(firstSegment))
27
+ segmentHasNested.set(firstSegment, false);
28
+ if (segments.length > 1)
29
+ segmentHasNested.set(firstSegment, true);
30
+ }
31
+ return pages.sort((a, b) => {
32
+ const segmentsA = getPathSegments(a.pathname);
33
+ const segmentsB = getPathSegments(b.pathname);
34
+ const firstSegmentA = segmentsA[0] || "";
35
+ const firstSegmentB = segmentsB[0] || "";
36
+ const twoSegPrefixA = getGroupPrefix(a.pathname, 2);
37
+ const twoSegPrefixB = getGroupPrefix(b.pathname, 2);
38
+ const twoSegCountA = twoSegmentCount.get(twoSegPrefixA) || 0;
39
+ const twoSegCountB = twoSegmentCount.get(twoSegPrefixB) || 0;
40
+ let groupKeyA = twoSegCountA > 1 ? twoSegPrefixA : `/${firstSegmentA}`;
41
+ let groupKeyB = twoSegCountB > 1 ? twoSegPrefixB : `/${firstSegmentB}`;
42
+ const isRootLevelA = segmentsA.length <= 1;
43
+ const isRootLevelB = segmentsB.length <= 1;
44
+ const hasNestedA = segmentHasNested.get(firstSegmentA);
45
+ const hasNestedB = segmentHasNested.get(firstSegmentB);
46
+ if (isRootLevelA && !hasNestedA)
47
+ groupKeyA = "";
48
+ if (isRootLevelB && !hasNestedB)
49
+ groupKeyB = "";
50
+ if (groupKeyA === "" && groupKeyB !== "")
51
+ return -1;
52
+ if (groupKeyA !== "" && groupKeyB === "")
53
+ return 1;
54
+ if (groupKeyA !== groupKeyB)
55
+ return groupKeyA.localeCompare(groupKeyB);
56
+ if (segmentsA.length === 0)
57
+ return -1;
58
+ if (segmentsB.length === 0)
59
+ return 1;
60
+ const minLen = Math.min(segmentsA.length, segmentsB.length);
61
+ for (let i = 0; i < minLen; i++) {
62
+ const cmp = segmentsA[i].localeCompare(segmentsB[i]);
63
+ if (cmp !== 0)
64
+ return cmp;
65
+ }
66
+ return segmentsA.length - segmentsB.length;
67
+ });
68
+ }
69
+ function getPageGroupKey(pathname, twoSegmentCount, segmentHasNested) {
70
+ const segments = getPathSegments(pathname);
71
+ const firstSegment = segments[0] || "";
72
+ const twoSegPrefix = getGroupPrefix(pathname, 2);
73
+ const twoSegCount = twoSegmentCount.get(twoSegPrefix) || 0;
74
+ let groupKey = twoSegCount > 1 ? twoSegPrefix : `/${firstSegment}`;
75
+ const isRootLevel = segments.length <= 1;
76
+ const hasNested = segmentHasNested.get(firstSegment);
77
+ if (isRootLevel && !hasNested)
78
+ groupKey = "";
79
+ return groupKey;
80
+ }
81
+ function formatPagesWithGroups(pages) {
82
+ if (pages.length === 0)
83
+ return [];
84
+ const twoSegmentCount = /* @__PURE__ */ new Map();
85
+ const segmentHasNested = /* @__PURE__ */ new Map();
86
+ for (const page of pages) {
87
+ const prefix = getGroupPrefix(page.pathname, 2);
88
+ twoSegmentCount.set(prefix, (twoSegmentCount.get(prefix) || 0) + 1);
89
+ const segments = getPathSegments(page.pathname);
90
+ const firstSegment = segments[0] || "";
91
+ if (!segmentHasNested.has(firstSegment))
92
+ segmentHasNested.set(firstSegment, false);
93
+ if (segments.length > 1)
94
+ segmentHasNested.set(firstSegment, true);
95
+ }
96
+ const lines = [];
97
+ let currentGroup = "";
98
+ let segmentGroupIndex = 0;
99
+ let urlsInCurrentGroup = 0;
100
+ for (const page of pages) {
101
+ const groupKey = getPageGroupKey(page.pathname, twoSegmentCount, segmentHasNested);
102
+ if (groupKey !== currentGroup) {
103
+ if (urlsInCurrentGroup > 0) {
104
+ const shouldAddBlankLine = segmentGroupIndex === 0 || segmentGroupIndex >= 1 && segmentGroupIndex <= 2 && urlsInCurrentGroup > 1;
105
+ if (shouldAddBlankLine)
106
+ lines.push("");
107
+ }
108
+ currentGroup = groupKey;
109
+ segmentGroupIndex++;
110
+ urlsInCurrentGroup = 0;
111
+ }
112
+ urlsInCurrentGroup++;
113
+ const descText = page.description ? `: ${page.description.substring(0, 160)}${page.description.length > 160 ? "..." : ""}` : "";
114
+ if (page.title && page.title !== page.pathname)
115
+ lines.push(`- [${page.title}](${page.pathname})${descText}`);
116
+ else
117
+ lines.push(`- ${page.pathname}${descText}`);
118
+ }
119
+ return lines;
120
+ }
5
121
  function normalizeLink(link) {
6
122
  const parts = [];
7
123
  parts.push(`- [${link.title}](${link.href})`);
@@ -71,43 +187,41 @@ Canonical Origin: ${siteConfig.url}`);
71
187
  }
72
188
  const pages = await getPages();
73
189
  const urls = await fetchSitemapUrls(event);
190
+ const errorRoutes = await getErrorRoutes();
74
191
  const devModeHint = import.meta.dev && pages.size === 0 ? " (dev mode - run `nuxi generate` for page titles)" : "";
75
192
  const prerendered = [];
193
+ const seenPaths = /* @__PURE__ */ new Set();
76
194
  for (const [pathname, page] of pages) {
77
- prerendered.push({ pathname, title: page.title });
195
+ prerendered.push({ pathname, title: page.title, description: page.description });
196
+ seenPaths.add(pathname);
78
197
  }
79
198
  const other = [];
80
199
  for (const url of urls) {
81
200
  const pathname = url.loc.startsWith("http") ? new URL(url.loc).pathname : url.loc;
82
- if (!pages.has(pathname)) {
83
- other.push(pathname);
201
+ if (!seenPaths.has(pathname) && !errorRoutes.has(pathname)) {
202
+ other.push({ pathname });
203
+ seenPaths.add(pathname);
84
204
  }
85
205
  }
86
- if (prerendered.length > 0 && other.length > 0) {
206
+ const sortedPrerendered = sortPagesByPath(prerendered);
207
+ const sortedOther = sortPagesByPath(other);
208
+ if (sortedPrerendered.length > 0 && sortedOther.length > 0) {
87
209
  parts.push(`## Prerendered Pages${devModeHint}
88
210
  `);
89
- for (const { pathname, title } of prerendered) {
90
- parts.push(title && title !== pathname ? `- [${title}](${pathname})` : `- ${pathname}`);
91
- }
211
+ parts.push(...formatPagesWithGroups(sortedPrerendered));
92
212
  parts.push("");
93
213
  parts.push("## Other Pages\n");
94
- for (const pathname of other) {
95
- parts.push(`- ${pathname}`);
96
- }
214
+ parts.push(...formatPagesWithGroups(sortedOther));
97
215
  parts.push("");
98
- } else if (prerendered.length > 0) {
216
+ } else if (sortedPrerendered.length > 0) {
99
217
  parts.push(`## Pages${devModeHint}
100
218
  `);
101
- for (const { pathname, title } of prerendered) {
102
- parts.push(title && title !== pathname ? `- [${title}](${pathname})` : `- ${pathname}`);
103
- }
219
+ parts.push(...formatPagesWithGroups(sortedPrerendered));
104
220
  parts.push("");
105
- } else if (other.length > 0) {
221
+ } else if (sortedOther.length > 0) {
106
222
  parts.push(`## Pages${devModeHint}
107
223
  `);
108
- for (const pathname of other) {
109
- parts.push(`- ${pathname}`);
110
- }
224
+ parts.push(...formatPagesWithGroups(sortedOther));
111
225
  parts.push("");
112
226
  }
113
227
  return parts.join("\n");
@@ -12,8 +12,6 @@ export interface PageData extends PageEntry {
12
12
  }
13
13
  /** Read page data - returns page data indexed by route */
14
14
  export declare function getPages(): Promise<Map<string, PageEntry>>;
15
- /** Get all page data including markdown (prerender only) */
16
- export declare function readPrerenderedPageData(): Promise<Map<string, PageData>>;
17
15
  /** Page list item for MCP tools/resources */
18
16
  export interface PageListItem {
19
17
  route: string;
@@ -23,3 +21,5 @@ export interface PageListItem {
23
21
  }
24
22
  /** Get pages as flat list for MCP consumption */
25
23
  export declare function getPagesList(): Promise<PageListItem[]>;
24
+ /** Get error routes detected during prerender */
25
+ export declare function getErrorRoutes(): Promise<Set<string>>;
@@ -2,17 +2,21 @@ export async function getPages() {
2
2
  if (import.meta.dev)
3
3
  return /* @__PURE__ */ new Map();
4
4
  if (import.meta.prerender) {
5
- return readPrerenderedPageData();
5
+ const data = await readPrerenderedData();
6
+ return data.pages;
6
7
  }
7
8
  const m = await import("#ai-ready-virtual/page-data.mjs");
8
9
  return m.pages?.length ? new Map(m.pages.map((p) => [p.route, p])) : /* @__PURE__ */ new Map();
9
10
  }
10
- export async function readPrerenderedPageData() {
11
+ async function readPrerenderedData() {
11
12
  if (!import.meta.prerender)
12
- return /* @__PURE__ */ new Map();
13
- const { readPageDataFromFilesystem } = await import("#ai-ready-virtual/read-page-data.mjs");
14
- const pages = await readPageDataFromFilesystem();
15
- return pages?.length ? new Map(pages.map((p) => [p.route, p])) : /* @__PURE__ */ new Map();
13
+ return { pages: /* @__PURE__ */ new Map(), errorRoutes: /* @__PURE__ */ new Set() };
14
+ const m = await import("#ai-ready-virtual/read-page-data.mjs");
15
+ const data = await m.readPageDataFromFilesystem();
16
+ return {
17
+ pages: data.pages?.length ? new Map(data.pages.map((p) => [p.route, p])) : /* @__PURE__ */ new Map(),
18
+ errorRoutes: new Set(data.errorRoutes || [])
19
+ };
16
20
  }
17
21
  export async function getPagesList() {
18
22
  const pages = await getPages();
@@ -23,3 +27,13 @@ export async function getPagesList() {
23
27
  headings: p.headings || void 0
24
28
  }));
25
29
  }
30
+ export async function getErrorRoutes() {
31
+ if (import.meta.dev)
32
+ return /* @__PURE__ */ new Set();
33
+ if (import.meta.prerender) {
34
+ const data = await readPrerenderedData();
35
+ return data.errorRoutes;
36
+ }
37
+ const m = await import("#ai-ready-virtual/page-data.mjs");
38
+ return new Set(m.errorRoutes || []);
39
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",