nuxt-ai-ready 0.2.4 → 0.3.1
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/README.md +2 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +197 -27
- package/dist/runtime/nuxt/plugins/prerender.d.ts +1 -1
- package/dist/runtime/server/mcp/dev/resources/pages.js +19 -0
- package/dist/runtime/server/mcp/dev/tools/list-pages.js +10 -0
- package/dist/runtime/server/mcp/dev/utils.js +22 -0
- package/dist/runtime/server/mcp/{tools → prod/tools}/list-pages.js +1 -1
- package/dist/runtime/server/plugins/sitemap-lastmod.d.ts +2 -0
- package/dist/runtime/server/plugins/sitemap-lastmod.js +22 -0
- package/dist/runtime/server/utils.d.ts +2 -1
- package/dist/runtime/server/utils.js +22 -9
- package/dist/runtime/types.d.ts +17 -14
- package/package.json +5 -4
- package/dist/runtime/server/mcp/resources/pages-chunks.d.ts +0 -17
- package/dist/runtime/server/mcp/resources/pages.d.ts +0 -17
- package/dist/runtime/server/mcp/tools/list-pages.d.ts +0 -86
- /package/dist/runtime/server/mcp/{resources → prod/resources}/pages-chunks.js +0 -0
- /package/dist/runtime/server/mcp/{resources → prod/resources}/pages.js +0 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
5
|
[![Nuxt][nuxt-src]][nuxt-href]
|
|
6
6
|
|
|
7
|
-
> Best practice AI & LLM discoverability for Nuxt sites
|
|
7
|
+
> Best practice AI & LLM discoverability for Nuxt sites
|
|
8
8
|
|
|
9
9
|
## Why Nuxt AI Ready?
|
|
10
10
|
|
|
@@ -27,6 +27,7 @@ Nuxt AI Ready converts your indexable pages into clean markdown that AI systems
|
|
|
27
27
|
- 🌐 **Indexable Pages**: Integrating with [Nuxt Sitemap](https://nuxtseo.com/sitemap) to index only AI-allowed pages
|
|
28
28
|
- 📦 **Bulk Chunk Export**: Exported token optimized chunks ready for RAG and semantic search
|
|
29
29
|
- ⚡ **MCP Integration**: Let AI agents query your site directly
|
|
30
|
+
- ⏱️ **Automatic Last Updated**: Content freshness automatically tracked and added to your [sitemap.xml](https://nuxtseo.com/docs/ai-ready/guides/automatic-updated-at)
|
|
30
31
|
|
|
31
32
|
## Installation
|
|
32
33
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,17 +1,118 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { useLogger, useNuxt, defineNuxtModule, createResolver, addTypeTemplate, hasNuxtModule, addServerHandler, addPlugin, extendPages } from '@nuxt/kit';
|
|
2
3
|
import defu from 'defu';
|
|
3
4
|
import { useSiteConfig, installNuxtSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
|
|
4
5
|
import { relative } from 'pathe';
|
|
5
6
|
import { readPackageJSON } from 'pkg-types';
|
|
6
7
|
import { createHash } from 'node:crypto';
|
|
7
8
|
import { mkdirSync, createWriteStream } from 'node:fs';
|
|
8
|
-
import { stat, open } from 'node:fs/promises';
|
|
9
|
-
import { join, dirname } from 'node:path';
|
|
9
|
+
import { mkdir, stat, open } from 'node:fs/promises';
|
|
10
10
|
import { encodeLines } from '@toon-format/toon';
|
|
11
11
|
import { createLlmsTxtStream } from 'mdream/llms-txt';
|
|
12
|
+
import { createStorage } from 'unstorage';
|
|
13
|
+
import fsDriver from 'unstorage/drivers/fs';
|
|
12
14
|
|
|
13
15
|
const logger = useLogger("nuxt-ai-ready");
|
|
14
16
|
|
|
17
|
+
function createContentHashManager(options) {
|
|
18
|
+
const { storagePath, debug = false } = options;
|
|
19
|
+
let storage;
|
|
20
|
+
let manifest = {
|
|
21
|
+
pages: {},
|
|
22
|
+
version: "1"
|
|
23
|
+
};
|
|
24
|
+
async function initStorage() {
|
|
25
|
+
await mkdir(dirname(storagePath), { recursive: true });
|
|
26
|
+
storage = createStorage({
|
|
27
|
+
driver: fsDriver({ base: dirname(storagePath) })
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function hashContent(markdown) {
|
|
31
|
+
return createHash("sha256").update(markdown).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
async function getManifest() {
|
|
34
|
+
if (!storage) {
|
|
35
|
+
await initStorage();
|
|
36
|
+
}
|
|
37
|
+
const stored = await storage.getItem("content-hashes.json");
|
|
38
|
+
if (stored) {
|
|
39
|
+
manifest = stored;
|
|
40
|
+
if (debug) {
|
|
41
|
+
logger.debug(`Loaded manifest with ${Object.keys(manifest.pages).length} pages`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
if (debug) {
|
|
45
|
+
logger.debug("No existing manifest found, starting fresh");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return manifest;
|
|
49
|
+
}
|
|
50
|
+
async function saveManifest() {
|
|
51
|
+
if (!storage) {
|
|
52
|
+
await initStorage();
|
|
53
|
+
}
|
|
54
|
+
await storage.setItem("content-hashes.json", manifest);
|
|
55
|
+
if (debug) {
|
|
56
|
+
logger.debug(`Saved manifest with ${Object.keys(manifest.pages).length} pages`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function updatePageHash(route, markdown, previousManifest) {
|
|
60
|
+
const contentHash = hashContent(markdown);
|
|
61
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
62
|
+
const existing = previousManifest.pages[route];
|
|
63
|
+
let result;
|
|
64
|
+
if (!existing) {
|
|
65
|
+
result = {
|
|
66
|
+
contentHash,
|
|
67
|
+
updatedAt: now,
|
|
68
|
+
firstSeenAt: now
|
|
69
|
+
};
|
|
70
|
+
if (debug) {
|
|
71
|
+
logger.debug(`New page detected: ${route}`);
|
|
72
|
+
}
|
|
73
|
+
} else if (existing.contentHash !== contentHash) {
|
|
74
|
+
result = {
|
|
75
|
+
contentHash,
|
|
76
|
+
updatedAt: now,
|
|
77
|
+
firstSeenAt: existing.firstSeenAt
|
|
78
|
+
};
|
|
79
|
+
if (debug) {
|
|
80
|
+
logger.debug(`Content changed: ${route}`);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
result = {
|
|
84
|
+
contentHash: existing.contentHash,
|
|
85
|
+
updatedAt: existing.updatedAt,
|
|
86
|
+
firstSeenAt: existing.firstSeenAt
|
|
87
|
+
};
|
|
88
|
+
if (debug) {
|
|
89
|
+
logger.debug(`Content unchanged: ${route}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
manifest.pages[route] = result;
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
function setPageTimestamp(route, markdown, timestamp, previousManifest) {
|
|
96
|
+
const contentHash = hashContent(markdown);
|
|
97
|
+
const existing = previousManifest.pages[route];
|
|
98
|
+
manifest.pages[route] = {
|
|
99
|
+
contentHash,
|
|
100
|
+
updatedAt: timestamp,
|
|
101
|
+
firstSeenAt: existing?.firstSeenAt || timestamp
|
|
102
|
+
};
|
|
103
|
+
if (debug) {
|
|
104
|
+
logger.debug(`Manual timestamp set for ${route}: ${timestamp}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
getManifest,
|
|
109
|
+
saveManifest,
|
|
110
|
+
hashContent,
|
|
111
|
+
updatePageHash,
|
|
112
|
+
setPageTimestamp
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
15
116
|
function generateVectorId(route, chunkIdx) {
|
|
16
117
|
const hash = createHash("sha256").update(route).digest("hex").substring(0, 8);
|
|
17
118
|
return `${hash}-${chunkIdx}`;
|
|
@@ -30,7 +131,7 @@ async function updateFirstLine(filePath, newFirstLine) {
|
|
|
30
131
|
await fh.close();
|
|
31
132
|
}
|
|
32
133
|
}
|
|
33
|
-
function setupPrerenderHandler(llmsTxtConfig) {
|
|
134
|
+
function setupPrerenderHandler(llmsTxtConfig, timestampsConfig) {
|
|
34
135
|
const nuxt = useNuxt();
|
|
35
136
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
36
137
|
let writer = null;
|
|
@@ -41,6 +142,18 @@ function setupPrerenderHandler(llmsTxtConfig) {
|
|
|
41
142
|
const startTime = Date.now();
|
|
42
143
|
const pagesChunksPath = join(nitro.options.output.publicDir, "llms-full.toon");
|
|
43
144
|
const pagesPath = join(nitro.options.output.publicDir, "llms.toon");
|
|
145
|
+
let contentHashManager = null;
|
|
146
|
+
let previousManifest = null;
|
|
147
|
+
if (timestampsConfig?.enabled) {
|
|
148
|
+
const manifestPath = join(
|
|
149
|
+
nuxt.options.rootDir,
|
|
150
|
+
timestampsConfig.manifestPath || "node_modules/.cache/nuxt-seo/ai-index/content-hashes.json"
|
|
151
|
+
);
|
|
152
|
+
contentHashManager = createContentHashManager({
|
|
153
|
+
storagePath: manifestPath,
|
|
154
|
+
debug: !!nuxt.options.debug
|
|
155
|
+
});
|
|
156
|
+
}
|
|
44
157
|
nitro.hooks.hook("prerender:generate", async (route) => {
|
|
45
158
|
if (!route.fileName?.endsWith(".md")) {
|
|
46
159
|
return;
|
|
@@ -61,15 +174,39 @@ function setupPrerenderHandler(llmsTxtConfig) {
|
|
|
61
174
|
notes: llmsTxtConfig.notes
|
|
62
175
|
});
|
|
63
176
|
writer = stream.getWriter();
|
|
177
|
+
if (contentHashManager && !previousManifest) {
|
|
178
|
+
previousManifest = await contentHashManager.getManifest();
|
|
179
|
+
}
|
|
64
180
|
mkdirSync(dirname(pagesChunksPath), { recursive: true });
|
|
65
181
|
mkdirSync(dirname(pagesPath), { recursive: true });
|
|
66
182
|
chunksStream = createWriteStream(pagesChunksPath, { encoding: "utf-8" });
|
|
67
183
|
chunksStream.write("pageChunks[999999]{id,route,content}:\n");
|
|
68
184
|
pagesStream = createWriteStream(pagesPath, { encoding: "utf-8" });
|
|
69
|
-
pagesStream.write("pages[999999]{route,title,description,headings,chunkIds}:\n");
|
|
185
|
+
pagesStream.write("pages[999999]{route,title,description,headings,chunkIds,updatedAt}:\n");
|
|
70
186
|
}
|
|
71
|
-
const { chunks, title, description, headings } = JSON.parse(route.contents || "{}");
|
|
187
|
+
const { chunks, title, description, headings, updatedAt: metaUpdatedAt } = JSON.parse(route.contents || "{}");
|
|
72
188
|
const markdown = chunks.map((c) => c.content).join("\n\n");
|
|
189
|
+
let pageTimestamp = {
|
|
190
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
191
|
+
};
|
|
192
|
+
let usedMetaTimestamp = false;
|
|
193
|
+
if (metaUpdatedAt) {
|
|
194
|
+
const parsedDate = new Date(metaUpdatedAt);
|
|
195
|
+
if (!Number.isNaN(parsedDate.getTime())) {
|
|
196
|
+
pageTimestamp.updatedAt = parsedDate.toISOString();
|
|
197
|
+
usedMetaTimestamp = true;
|
|
198
|
+
if (contentHashManager && previousManifest) {
|
|
199
|
+
contentHashManager.setPageTimestamp(pageRoute, markdown, pageTimestamp.updatedAt, previousManifest);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!usedMetaTimestamp && contentHashManager && previousManifest) {
|
|
204
|
+
pageTimestamp = contentHashManager.updatePageHash(
|
|
205
|
+
pageRoute,
|
|
206
|
+
markdown,
|
|
207
|
+
previousManifest
|
|
208
|
+
);
|
|
209
|
+
}
|
|
73
210
|
await writer.write({
|
|
74
211
|
url: pageRoute,
|
|
75
212
|
title,
|
|
@@ -121,7 +258,8 @@ function setupPrerenderHandler(llmsTxtConfig) {
|
|
|
121
258
|
([tag, texts]) => texts.map((text) => `${tag}:${text}`)
|
|
122
259
|
).join("|") : "",
|
|
123
260
|
// Join chunkIds array to comma-separated string
|
|
124
|
-
chunkIds: chunkIds.join(",")
|
|
261
|
+
chunkIds: chunkIds.join(","),
|
|
262
|
+
updatedAt: pageTimestamp.updatedAt
|
|
125
263
|
};
|
|
126
264
|
if (pagesStream) {
|
|
127
265
|
const lines = Array.from(encodeLines({ pages: [pageDoc] }));
|
|
@@ -152,7 +290,11 @@ function setupPrerenderHandler(llmsTxtConfig) {
|
|
|
152
290
|
});
|
|
153
291
|
}
|
|
154
292
|
await updateFirstLine(pagesChunksPath, `pageChunks[${chunksProcessed}]{id,route,content}:`);
|
|
155
|
-
await updateFirstLine(pagesPath, `pages[${pageCount}]{route,title,description,headings,chunkIds}:`);
|
|
293
|
+
await updateFirstLine(pagesPath, `pages[${pageCount}]{route,title,description,headings,chunkIds,updatedAt}:`);
|
|
294
|
+
if (contentHashManager) {
|
|
295
|
+
await contentHashManager.saveManifest();
|
|
296
|
+
logger.debug("Saved content hash manifest");
|
|
297
|
+
}
|
|
156
298
|
logger.info(`Wrote llms-full.toon with ${chunksProcessed} chunks`);
|
|
157
299
|
logger.info(`Wrote llms.toon with ${pageCount} pages`);
|
|
158
300
|
const llmsTxtPath = join(nitro.options.output.publicDir, "llms.txt");
|
|
@@ -196,6 +338,23 @@ function setupPrerenderHandler(llmsTxtConfig) {
|
|
|
196
338
|
});
|
|
197
339
|
}
|
|
198
340
|
|
|
341
|
+
function createPagesPromise(nuxt = useNuxt()) {
|
|
342
|
+
return new Promise((resolve) => {
|
|
343
|
+
nuxt.hooks.hook("modules:done", () => {
|
|
344
|
+
if (typeof nuxt.options.pages === "boolean" && !nuxt.options.pages || typeof nuxt.options.pages === "object" && !nuxt.options.pages.enabled) {
|
|
345
|
+
return resolve([]);
|
|
346
|
+
}
|
|
347
|
+
extendPages(resolve);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function flattenPages(pages, parent = "") {
|
|
352
|
+
return pages.flatMap((page) => {
|
|
353
|
+
const path = parent + page.path;
|
|
354
|
+
const current = { path, name: page.name, meta: page.meta };
|
|
355
|
+
return page.children?.length ? [current, ...flattenPages(page.children, path)] : [current];
|
|
356
|
+
});
|
|
357
|
+
}
|
|
199
358
|
const module$1 = defineNuxtModule({
|
|
200
359
|
meta: {
|
|
201
360
|
name: "nuxt-ai-ready",
|
|
@@ -227,6 +386,10 @@ const module$1 = defineNuxtModule({
|
|
|
227
386
|
maxAge: 3600,
|
|
228
387
|
// 1 hour
|
|
229
388
|
swr: true
|
|
389
|
+
},
|
|
390
|
+
timestamps: {
|
|
391
|
+
enabled: false,
|
|
392
|
+
manifestPath: "node_modules/.cache/nuxt-seo/ai-index/content-hashes.json"
|
|
230
393
|
}
|
|
231
394
|
};
|
|
232
395
|
},
|
|
@@ -247,9 +410,17 @@ const module$1 = defineNuxtModule({
|
|
|
247
410
|
}
|
|
248
411
|
nuxt.options.nitro.scanDirs = nuxt.options.nitro.scanDirs || [];
|
|
249
412
|
nuxt.options.nitro.scanDirs.push(
|
|
250
|
-
resolve("./runtime/server/utils")
|
|
251
|
-
resolve("./runtime/server/mcp")
|
|
413
|
+
resolve("./runtime/server/utils")
|
|
252
414
|
);
|
|
415
|
+
const pagesPromise = createPagesPromise(nuxt);
|
|
416
|
+
nuxt.hooks.hook("nitro:config", (nitroConfig) => {
|
|
417
|
+
nitroConfig.virtual = nitroConfig.virtual || {};
|
|
418
|
+
nitroConfig.virtual["#ai-ready/routes.mjs"] = async () => {
|
|
419
|
+
const pages = await pagesPromise;
|
|
420
|
+
const routes = flattenPages(pages);
|
|
421
|
+
return `export default ${JSON.stringify(routes)}`;
|
|
422
|
+
};
|
|
423
|
+
});
|
|
253
424
|
if (typeof config.contentSignal === "object") {
|
|
254
425
|
nuxt.options.robots.groups.push({
|
|
255
426
|
userAgent: "*",
|
|
@@ -305,22 +476,12 @@ export {}
|
|
|
305
476
|
const hasMCP = hasNuxtModule("@nuxtjs/mcp-toolkit");
|
|
306
477
|
if (hasMCP) {
|
|
307
478
|
nuxt.hook("mcp:definitions:paths", (paths) => {
|
|
308
|
-
const mcpRuntimeDir = resolve(
|
|
309
|
-
paths.tools = paths.tools || [];
|
|
310
|
-
paths.resources = paths.resources || [];
|
|
311
|
-
paths.prompts = paths.prompts || [];
|
|
479
|
+
const mcpRuntimeDir = resolve(`./runtime/server/mcp/${nuxt.options.dev ? "dev" : "prod"}`);
|
|
312
480
|
const mcpConfig = config.mcp || {};
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
paths.
|
|
317
|
-
}
|
|
318
|
-
if (resourcesConfig.pages !== false) {
|
|
319
|
-
paths.resources.push(`${mcpRuntimeDir}/resources/pages.ts`);
|
|
320
|
-
}
|
|
321
|
-
if (resourcesConfig.pagesChunks !== false) {
|
|
322
|
-
paths.resources.push(`${mcpRuntimeDir}/resources/pages-chunks.ts`);
|
|
323
|
-
}
|
|
481
|
+
if (mcpConfig.tools !== false)
|
|
482
|
+
(paths.tools ||= []).push(`${mcpRuntimeDir}/tools`);
|
|
483
|
+
if (mcpConfig.resources !== false)
|
|
484
|
+
(paths.resources ||= []).push(`${mcpRuntimeDir}/resources`);
|
|
324
485
|
});
|
|
325
486
|
const mcpLink = {
|
|
326
487
|
title: "MCP",
|
|
@@ -350,16 +511,25 @@ export {}
|
|
|
350
511
|
await nuxt.callHook("ai-ready:llms-txt", llmsTxtPayload);
|
|
351
512
|
mergedLlmsTxt.sections = llmsTxtPayload.sections;
|
|
352
513
|
mergedLlmsTxt.notes = llmsTxtPayload.notes.length > 0 ? llmsTxtPayload.notes : void 0;
|
|
514
|
+
const timestampsManifestPath = config.timestamps?.enabled ? join(nuxt.options.rootDir, config.timestamps.manifestPath || "node_modules/.cache/nuxt-seo/ai-index/content-hashes.json") : void 0;
|
|
353
515
|
nuxt.options.runtimeConfig["nuxt-ai-ready"] = {
|
|
354
516
|
version: version || "0.0.0",
|
|
355
517
|
debug: config.debug || false,
|
|
518
|
+
hasSitemap: hasNuxtModule("@nuxtjs/sitemap"),
|
|
356
519
|
mdreamOptions: config.mdreamOptions || {},
|
|
357
520
|
markdownCacheHeaders: defu(config.markdownCacheHeaders, {
|
|
358
521
|
maxAge: 3600,
|
|
359
522
|
swr: true
|
|
360
523
|
}),
|
|
361
|
-
llmsTxt: mergedLlmsTxt
|
|
524
|
+
llmsTxt: mergedLlmsTxt,
|
|
525
|
+
timestampsManifestPath
|
|
362
526
|
};
|
|
527
|
+
if (config.timestamps?.enabled && hasNuxtModule("@nuxtjs/sitemap")) {
|
|
528
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
529
|
+
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
530
|
+
nitroConfig.plugins.push(resolve("./runtime/server/plugins/sitemap-lastmod"));
|
|
531
|
+
});
|
|
532
|
+
}
|
|
363
533
|
addServerHandler({
|
|
364
534
|
middleware: true,
|
|
365
535
|
handler: resolve("./runtime/server/middleware/mdream")
|
|
@@ -371,7 +541,7 @@ export {}
|
|
|
371
541
|
addServerHandler({ route: "/llms-full.txt", handler: resolve("./runtime/server/routes/llms.txt.get") });
|
|
372
542
|
const isStatic = nuxt.options.nitro.static || nuxt.options._generate || false;
|
|
373
543
|
if (isStatic || nuxt.options.nitro.prerender?.routes?.length) {
|
|
374
|
-
setupPrerenderHandler(mergedLlmsTxt);
|
|
544
|
+
setupPrerenderHandler(mergedLlmsTxt, config.timestamps);
|
|
375
545
|
}
|
|
376
546
|
nuxt.options.nitro.routeRules = nuxt.options.nitro.routeRules || {};
|
|
377
547
|
nuxt.options.nitro.routeRules["/llms.toon"] = { headers: { "Content-Type": "text/toon; charset=utf-8" } };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getDevPages } from "../utils.js";
|
|
2
|
+
export default {
|
|
3
|
+
uri: "resource://nuxt-ai-ready/pages",
|
|
4
|
+
name: "All Pages",
|
|
5
|
+
description: "Page routes from sitemap/routes. In dev mode, returns JSON (TOON format unavailable until build).",
|
|
6
|
+
metadata: {
|
|
7
|
+
mimeType: "application/json"
|
|
8
|
+
},
|
|
9
|
+
async handler(uri) {
|
|
10
|
+
const pages = await getDevPages();
|
|
11
|
+
return {
|
|
12
|
+
contents: [{
|
|
13
|
+
uri: uri.toString(),
|
|
14
|
+
mimeType: "application/json",
|
|
15
|
+
text: JSON.stringify(pages, null, 2)
|
|
16
|
+
}]
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getDevPages, jsonResult } from "../utils.js";
|
|
2
|
+
export default {
|
|
3
|
+
name: "list_pages",
|
|
4
|
+
description: "Lists all available pages with their routes. In dev mode, returns JSON from sitemap/routes (TOON format unavailable until build).",
|
|
5
|
+
inputSchema: {},
|
|
6
|
+
async handler() {
|
|
7
|
+
const pages = await getDevPages();
|
|
8
|
+
return jsonResult(pages);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import routes from "#ai-ready/routes.mjs";
|
|
2
|
+
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
|
+
export { jsonResult } from "../utils.js";
|
|
4
|
+
export async function getDevPages() {
|
|
5
|
+
const config = useRuntimeConfig()["nuxt-ai-ready"];
|
|
6
|
+
if (!config.hasSitemap)
|
|
7
|
+
return routes.map((r) => ({ route: r.path, name: r.name, meta: r.meta }));
|
|
8
|
+
const { parseSitemapXml } = await import("@nuxtjs/sitemap/utils");
|
|
9
|
+
const sitemapRes = await fetch("/sitemap.xml");
|
|
10
|
+
if (!sitemapRes.ok)
|
|
11
|
+
return routes.map((r) => ({ route: r.path, name: r.name, meta: r.meta }));
|
|
12
|
+
const xml = await sitemapRes.text();
|
|
13
|
+
const { urls } = await parseSitemapXml(xml);
|
|
14
|
+
return urls.map((entry) => {
|
|
15
|
+
if (typeof entry === "string")
|
|
16
|
+
return { route: new URL(entry).pathname };
|
|
17
|
+
return {
|
|
18
|
+
route: new URL(entry.loc).pathname,
|
|
19
|
+
lastmod: entry.lastmod
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { toonResult } from "
|
|
2
|
+
import { toonResult } from "../../utils.js";
|
|
3
3
|
const schema = {
|
|
4
4
|
mode: z.enum(["chunks", "minimal"]).default("minimal").describe("Return individual content chunks (chunks) or page-level metadata (minimal)")
|
|
5
5
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
|
|
3
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
4
|
+
const config = useRuntimeConfig();
|
|
5
|
+
const manifestPath = config["nuxt-ai-ready"]?.timestampsManifestPath;
|
|
6
|
+
if (!manifestPath) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
nitroApp.hooks.hook("sitemap:resolved", async (ctx) => {
|
|
10
|
+
const manifest = await readFile(manifestPath, "utf-8").then((data) => JSON.parse(data)).catch(() => null);
|
|
11
|
+
if (!manifest) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const url of ctx.urls) {
|
|
15
|
+
const route = url.loc.replace(/^https?:\/\/[^/]+/, "").replace(/\/$/, "") || "/";
|
|
16
|
+
const pageData = manifest.pages[route];
|
|
17
|
+
if (pageData?.updatedAt) {
|
|
18
|
+
url.lastmod = pageData.updatedAt;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ModulePublicRuntimeConfig } from '../../module.js';
|
|
2
2
|
export declare function convertHtmlToMarkdownChunks(html: string, url: string, mdreamOptions: ModulePublicRuntimeConfig['mdreamOptions']): Promise<{
|
|
3
|
+
headings: Record<string, string[]>;
|
|
4
|
+
updatedAt?: string | undefined;
|
|
3
5
|
chunks: import("mdream").MarkdownChunk[];
|
|
4
6
|
title: string;
|
|
5
7
|
description: string;
|
|
6
|
-
headings: Record<string, string[]>;
|
|
7
8
|
}>;
|
|
@@ -6,12 +6,19 @@ import { estimateTokenCount } from "tokenx";
|
|
|
6
6
|
export async function convertHtmlToMarkdownChunks(html, url, mdreamOptions) {
|
|
7
7
|
let title = "";
|
|
8
8
|
let description = "";
|
|
9
|
+
let updatedAt;
|
|
9
10
|
const extractPlugin = extractionPlugin({
|
|
10
11
|
title(el) {
|
|
11
12
|
title = el.textContent;
|
|
12
13
|
},
|
|
13
14
|
'meta[name="description"]': (el) => {
|
|
14
15
|
description = el.attributes.content || "";
|
|
16
|
+
},
|
|
17
|
+
// Extract timestamp from various meta tag formats
|
|
18
|
+
'meta[property="article:modified_time"], meta[name="last-modified"], meta[name="updated"], meta[property="og:updated_time"], meta[name="lastmod"]': (el) => {
|
|
19
|
+
if (!updatedAt && el.attributes.content) {
|
|
20
|
+
updatedAt = el.attributes.content;
|
|
21
|
+
}
|
|
15
22
|
}
|
|
16
23
|
});
|
|
17
24
|
let options = {
|
|
@@ -38,13 +45,19 @@ export async function convertHtmlToMarkdownChunks(html, url, mdreamOptions) {
|
|
|
38
45
|
for await (const chunk of chunksStream) {
|
|
39
46
|
chunks.push(chunk);
|
|
40
47
|
}
|
|
41
|
-
return {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
return {
|
|
49
|
+
chunks,
|
|
50
|
+
title,
|
|
51
|
+
description,
|
|
52
|
+
...updatedAt && { updatedAt },
|
|
53
|
+
headings: chunks.reduce((set, m) => {
|
|
54
|
+
Object.entries(m.metadata?.headers || {}).forEach(([k, v]) => {
|
|
55
|
+
if (!set[k])
|
|
56
|
+
set[k] = [];
|
|
57
|
+
if (v && !set[k].includes(v))
|
|
58
|
+
set[k].push(v);
|
|
59
|
+
});
|
|
60
|
+
return set;
|
|
61
|
+
}, {})
|
|
62
|
+
};
|
|
50
63
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -63,24 +63,25 @@ export interface ModuleOptions {
|
|
|
63
63
|
* @default All enabled when @nuxtjs/mcp-toolkit is installed
|
|
64
64
|
*/
|
|
65
65
|
mcp?: {
|
|
66
|
+
/** Enable MCP tools (list-pages) @default true */
|
|
67
|
+
tools?: boolean;
|
|
68
|
+
/** Enable MCP resources (pages, pages-chunks) @default true */
|
|
69
|
+
resources?: boolean;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Content timestamp tracking configuration
|
|
73
|
+
*/
|
|
74
|
+
timestamps?: {
|
|
66
75
|
/**
|
|
67
|
-
* Enable
|
|
68
|
-
* @default
|
|
76
|
+
* Enable timestamp tracking
|
|
77
|
+
* @default false
|
|
69
78
|
*/
|
|
70
|
-
|
|
71
|
-
/** Get page by route - fetches markdown content for specific page */
|
|
72
|
-
listPages?: boolean;
|
|
73
|
-
};
|
|
79
|
+
enabled?: boolean;
|
|
74
80
|
/**
|
|
75
|
-
*
|
|
76
|
-
* @default
|
|
81
|
+
* Path to store content hash manifest
|
|
82
|
+
* @default 'node_modules/.cache/nuxt-seo/ai-index/content-hashes.json'
|
|
77
83
|
*/
|
|
78
|
-
|
|
79
|
-
/** pages://list - all pages without markdown content */
|
|
80
|
-
pages?: boolean;
|
|
81
|
-
/** pages://chunks - individual content chunks from all pages */
|
|
82
|
-
pagesChunks?: boolean;
|
|
83
|
-
};
|
|
84
|
+
manifestPath?: string;
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
/**
|
|
@@ -112,6 +113,8 @@ export interface BulkDocument {
|
|
|
112
113
|
headings: Array<Record<string, string>>;
|
|
113
114
|
/** All chunk IDs for this page (first ID can be used as document ID) */
|
|
114
115
|
chunkIds: string[];
|
|
116
|
+
/** ISO 8601 timestamp of last content update */
|
|
117
|
+
updatedAt?: string;
|
|
115
118
|
}
|
|
116
119
|
/**
|
|
117
120
|
* Hook context for markdown processing (Nitro runtime hook)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ai-ready",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "Best practice AI & LLM discoverability for Nuxt sites.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -43,10 +43,11 @@
|
|
|
43
43
|
"pkg-types": "^2.3.0",
|
|
44
44
|
"std-env": "^3.10.0",
|
|
45
45
|
"tokenx": "^1.2.1",
|
|
46
|
-
"ufo": "^1.6.1"
|
|
46
|
+
"ufo": "^1.6.1",
|
|
47
|
+
"unstorage": "^1.17.3"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
|
-
"@antfu/eslint-config": "^6.4.
|
|
50
|
+
"@antfu/eslint-config": "^6.4.2",
|
|
50
51
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
51
52
|
"@headlessui/vue": "^1.7.23",
|
|
52
53
|
"@nuxt/content": "^3.9.0",
|
|
@@ -75,7 +76,7 @@
|
|
|
75
76
|
"vitest": "^4.0.15",
|
|
76
77
|
"vue": "^3.5.25",
|
|
77
78
|
"vue-router": "^4.6.3",
|
|
78
|
-
"vue-tsc": "^3.1.
|
|
79
|
+
"vue-tsc": "^3.1.6",
|
|
79
80
|
"zod": "^4.1.13"
|
|
80
81
|
},
|
|
81
82
|
"resolutions": {
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
uri: string;
|
|
3
|
-
name: string;
|
|
4
|
-
description: string;
|
|
5
|
-
metadata: {
|
|
6
|
-
mimeType: string;
|
|
7
|
-
};
|
|
8
|
-
cache: "1h";
|
|
9
|
-
handler(uri: URL): Promise<{
|
|
10
|
-
contents: {
|
|
11
|
-
uri: string;
|
|
12
|
-
mimeType: string;
|
|
13
|
-
text: string;
|
|
14
|
-
}[];
|
|
15
|
-
}>;
|
|
16
|
-
};
|
|
17
|
-
export default _default;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
uri: string;
|
|
3
|
-
name: string;
|
|
4
|
-
description: string;
|
|
5
|
-
metadata: {
|
|
6
|
-
mimeType: string;
|
|
7
|
-
};
|
|
8
|
-
cache: "1h";
|
|
9
|
-
handler(uri: URL): Promise<{
|
|
10
|
-
contents: {
|
|
11
|
-
uri: string;
|
|
12
|
-
mimeType: string;
|
|
13
|
-
text: string;
|
|
14
|
-
}[];
|
|
15
|
-
}>;
|
|
16
|
-
};
|
|
17
|
-
export default _default;
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
/**
|
|
3
|
-
* Lists all pages by fetching and returning TOON-encoded data
|
|
4
|
-
* TOON (Token-Oriented Object Notation) is a compact encoding that minimizes tokens for LLM input
|
|
5
|
-
* See https://toonformat.dev
|
|
6
|
-
*/
|
|
7
|
-
declare const _default: {
|
|
8
|
-
name: string;
|
|
9
|
-
description: string;
|
|
10
|
-
inputSchema: {
|
|
11
|
-
mode: z.ZodDefault<z.ZodEnum<{
|
|
12
|
-
minimal: "minimal";
|
|
13
|
-
chunks: "chunks";
|
|
14
|
-
}>>;
|
|
15
|
-
};
|
|
16
|
-
cache: "1h";
|
|
17
|
-
handler({ mode }: import("@modelcontextprotocol/sdk/server/zod-compat.js").ShapeOutput<Readonly<{
|
|
18
|
-
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
19
|
-
}>>): Promise<{
|
|
20
|
-
[x: string]: unknown;
|
|
21
|
-
content: ({
|
|
22
|
-
type: "text";
|
|
23
|
-
text: string;
|
|
24
|
-
_meta?: {
|
|
25
|
-
[x: string]: unknown;
|
|
26
|
-
} | undefined;
|
|
27
|
-
} | {
|
|
28
|
-
type: "image";
|
|
29
|
-
data: string;
|
|
30
|
-
mimeType: string;
|
|
31
|
-
_meta?: {
|
|
32
|
-
[x: string]: unknown;
|
|
33
|
-
} | undefined;
|
|
34
|
-
} | {
|
|
35
|
-
type: "audio";
|
|
36
|
-
data: string;
|
|
37
|
-
mimeType: string;
|
|
38
|
-
_meta?: {
|
|
39
|
-
[x: string]: unknown;
|
|
40
|
-
} | undefined;
|
|
41
|
-
} | {
|
|
42
|
-
uri: string;
|
|
43
|
-
name: string;
|
|
44
|
-
type: "resource_link";
|
|
45
|
-
description?: string | undefined;
|
|
46
|
-
mimeType?: string | undefined;
|
|
47
|
-
_meta?: {
|
|
48
|
-
[x: string]: unknown;
|
|
49
|
-
} | undefined;
|
|
50
|
-
icons?: {
|
|
51
|
-
src: string;
|
|
52
|
-
mimeType?: string | undefined;
|
|
53
|
-
sizes?: string[] | undefined;
|
|
54
|
-
}[] | undefined;
|
|
55
|
-
title?: string | undefined;
|
|
56
|
-
} | {
|
|
57
|
-
type: "resource";
|
|
58
|
-
resource: {
|
|
59
|
-
uri: string;
|
|
60
|
-
text: string;
|
|
61
|
-
mimeType?: string | undefined;
|
|
62
|
-
_meta?: {
|
|
63
|
-
[x: string]: unknown;
|
|
64
|
-
} | undefined;
|
|
65
|
-
} | {
|
|
66
|
-
uri: string;
|
|
67
|
-
blob: string;
|
|
68
|
-
mimeType?: string | undefined;
|
|
69
|
-
_meta?: {
|
|
70
|
-
[x: string]: unknown;
|
|
71
|
-
} | undefined;
|
|
72
|
-
};
|
|
73
|
-
_meta?: {
|
|
74
|
-
[x: string]: unknown;
|
|
75
|
-
} | undefined;
|
|
76
|
-
})[];
|
|
77
|
-
_meta?: {
|
|
78
|
-
[x: string]: unknown;
|
|
79
|
-
} | undefined;
|
|
80
|
-
structuredContent?: {
|
|
81
|
-
[x: string]: unknown;
|
|
82
|
-
} | undefined;
|
|
83
|
-
isError?: boolean | undefined;
|
|
84
|
-
}>;
|
|
85
|
-
};
|
|
86
|
-
export default _default;
|
|
File without changes
|
|
File without changes
|