nuxt-ai-ready 0.0.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.
Files changed (34) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +63 -0
  3. package/dist/module.d.mts +62 -0
  4. package/dist/module.json +12 -0
  5. package/dist/module.mjs +406 -0
  6. package/dist/runtime/nuxt/plugins/prerender.d.ts +2 -0
  7. package/dist/runtime/nuxt/plugins/prerender.js +20 -0
  8. package/dist/runtime/server/logger.d.ts +1 -0
  9. package/dist/runtime/server/logger.js +4 -0
  10. package/dist/runtime/server/mcp/prompts/explain-concept.d.ts +2 -0
  11. package/dist/runtime/server/mcp/prompts/explain-concept.js +62 -0
  12. package/dist/runtime/server/mcp/prompts/find-information.d.ts +2 -0
  13. package/dist/runtime/server/mcp/prompts/find-information.js +57 -0
  14. package/dist/runtime/server/mcp/prompts/search-content.d.ts +2 -0
  15. package/dist/runtime/server/mcp/prompts/search-content.js +58 -0
  16. package/dist/runtime/server/mcp/resources/all-content.d.ts +2 -0
  17. package/dist/runtime/server/mcp/resources/all-content.js +14 -0
  18. package/dist/runtime/server/mcp/resources/pages.d.ts +2 -0
  19. package/dist/runtime/server/mcp/resources/pages.js +23 -0
  20. package/dist/runtime/server/mcp/tools/get-page.d.ts +2 -0
  21. package/dist/runtime/server/mcp/tools/get-page.js +42 -0
  22. package/dist/runtime/server/mcp/tools/list-pages.d.ts +2 -0
  23. package/dist/runtime/server/mcp/tools/list-pages.js +78 -0
  24. package/dist/runtime/server/middleware/mdream.d.ts +2 -0
  25. package/dist/runtime/server/middleware/mdream.js +132 -0
  26. package/dist/runtime/server/routes/llms.txt.get.d.ts +2 -0
  27. package/dist/runtime/server/routes/llms.txt.get.js +23 -0
  28. package/dist/runtime/server/tsconfig.json +3 -0
  29. package/dist/runtime/server/utils/db.d.ts +8 -0
  30. package/dist/runtime/server/utils/db.js +48 -0
  31. package/dist/runtime/types.d.ts +166 -0
  32. package/dist/runtime/types.js +0 -0
  33. package/dist/types.d.mts +12 -0
  34. package/package.json +99 -0
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) Harlan Wilton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Nuxt AI Ready
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![Nuxt][nuxt-src]][nuxt-href]
6
+
7
+ > Best practice AI & LLM discoverability for Nuxt sites
8
+
9
+ ## Why Nuxt AI Ready?
10
+
11
+ Search is changing. Outside of search engines, people now get answers directly from [ChatGPT](https://chatgpt.com/), [Claude](https://claude.ai/), and other AI assistants. When these LLMs answer questions about topics related to your site, you want your content to be the source they cite
12
+ to drive traffic and engagement back to you.
13
+
14
+ For that to happen, AI systems need to understand your content. New standards are being shaped like [llms.txt](https://llmstxt.org/) for discoverability and [MCP](https://modelcontextprotocol.io/) for letting agents query your site directly. But these standards are still evolving, and implementing them correctly
15
+ can be complex and time-consuming.
16
+
17
+ - **📈 Increase citations by LLMs**: AI assistants pull from sources they can parse easily. Structured, AI-friendly content gets referenced more often.
18
+ - **🔗 Direct site queries for LLMs**: MCP support means assistants can pull live data from you, not just static snippets from their training.
19
+
20
+ Nuxt AI Ready converts your indexable pages into clean markdown that AI systems can consume, generates the right artifacts at build time, and serves AI-friendly formats to bots automatically.
21
+
22
+ ## Features
23
+
24
+ - 📄 **llms.txt Generation**: Auto-generates `llms.txt` and `llms-full.txt` at build time
25
+ - 🚀 **On-Demand Markdown**: Any route available as `.md` (e.g., `/about` → `/about.md`)
26
+ - 🤖 **Smart Bot Detection**: Serves markdown to AI crawlers automatically
27
+ - 📡 **Content Signals**: Help AI systems understand how to use your pages
28
+ - 📦 **RAG-Ready Output**: Chunked content for semantic search and AI chat pipelines
29
+ - âš¡ **MCP Integration**: Let AI agents query your site directly
30
+
31
+ ## Installation
32
+
33
+ Install `nuxt-ai-ready` dependency to your project:
34
+
35
+ ```bash
36
+ npx nuxi@latest module add nuxt-ai-ready
37
+ ```
38
+
39
+ ## Documentation
40
+
41
+ [📖 Read the full documentation](https://nuxtseo.com/ai-ready) for more information.
42
+
43
+ ## Sponsors
44
+
45
+ <p align="center">
46
+ <a href="https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg">
47
+ <img src='https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg'/>
48
+ </a>
49
+ </p>
50
+
51
+ ## License
52
+
53
+ This module requires a [Nuxt SEO Pro license](https://nuxtseo.com/pro), see [LICENSE](https://github.com/harlan-zw/nuxt-ai-ready/blob/main/LICENSE) for full details.
54
+
55
+ <!-- Badges -->
56
+ [npm-version-src]: https://img.shields.io/npm/v/nuxt-ai-ready/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
57
+ [npm-version-href]: https://npmjs.com/package/nuxt-ai-ready
58
+
59
+ [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-ai-ready.svg?style=flat&colorA=18181B&colorB=28CF8D
60
+ [npm-downloads-href]: https://npmjs.com/package/nuxt-ai-ready
61
+
62
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt
63
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,62 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import { ModuleOptions, BulkChunk } from '../dist/runtime/types.js';
3
+ export { BulkChunk, ModuleOptions } from '../dist/runtime/types.js';
4
+ import { ProcessedFile } from 'mdream/llms-txt';
5
+
6
+ /**
7
+ * Hook payload for mdream:llms-txt
8
+ * Called after mdream has generated llms.txt, before writing to disk
9
+ *
10
+ * IMPORTANT: This uses a mutable pattern. Hooks should modify the content
11
+ * and fullContent properties directly rather than returning values.
12
+ *
13
+ * @example
14
+ * nuxt.hooks.hook('mdream:llms-txt', async (payload) => {
15
+ * payload.content += '\n\n## Custom Section\n\nAdded by hook!'
16
+ * payload.fullContent += '\n\n## Custom Section (Full)\n\nAdded by hook!'
17
+ * })
18
+ */
19
+ interface LlmsTxtGeneratePayload {
20
+ /** Current llms.txt content - modify this directly */
21
+ content: string;
22
+ /** Current llms-full.txt content - modify this directly */
23
+ fullContent: string;
24
+ /** All routes with their metadata (read-only) */
25
+ pages: ProcessedFile[];
26
+ }
27
+
28
+ interface ModuleHooks {
29
+ /**
30
+ * Hook to modify llms.txt content before final output
31
+ * Other modules can append their own API endpoints here
32
+ */
33
+ 'ai-ready:llms-txt': (payload: LlmsTxtGeneratePayload) => void | Promise<void>;
34
+ /**
35
+ * Hook to add routes to the AI ready
36
+ * Other modules can register their own API routes
37
+ */
38
+ 'ai-ready:routes': (payload: {
39
+ routes: Record<string, string>;
40
+ }) => void | Promise<void>;
41
+ /**
42
+ * Hook called for each chunk generated during prerendering for bulk export
43
+ */
44
+ 'ai-ready:chunk': (context: {
45
+ chunk: BulkChunk;
46
+ route: string;
47
+ title: string;
48
+ description: string;
49
+ headings: Array<Record<string, string>>;
50
+ }) => void | Promise<void>;
51
+ }
52
+ interface ModulePublicRuntimeConfig {
53
+ bulkRoute: string | false;
54
+ debug: boolean;
55
+ version: string;
56
+ mdreamOptions: ModuleOptions['mdreamOptions'];
57
+ markdownCacheHeaders: Required<NonNullable<ModuleOptions['markdownCacheHeaders']>>;
58
+ }
59
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
60
+
61
+ export { _default as default };
62
+ export type { ModuleHooks, ModulePublicRuntimeConfig };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-ai-ready",
3
+ "compatibility": {
4
+ "nuxt": ">=4.0.0"
5
+ },
6
+ "configKey": "aiReady",
7
+ "version": "0.0.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.2",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,406 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, createWriteStream } from 'node:fs';
3
+ import { useLogger, useNuxt, defineNuxtModule, createResolver, addTypeTemplate, hasNuxtModule, addServerHandler, addPlugin } from '@nuxt/kit';
4
+ import defu from 'defu';
5
+ import { TagIdMap } from 'mdream';
6
+ import { extractionPlugin } from 'mdream/plugins';
7
+ import { htmlToMarkdownSplitChunksStream } from 'mdream/splitter';
8
+ import { useSiteConfig, installNuxtSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
9
+ import { relative, resolve, dirname } from 'pathe';
10
+ import { readPackageJSON } from 'pkg-types';
11
+ import { estimateTokenCount } from 'tokenx';
12
+ import { writeFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { consola } from 'consola';
15
+ import { generateLlmsTxtArtifacts } from 'mdream/llms-txt';
16
+
17
+ const logger$1 = useLogger("nuxt-ai-ready");
18
+
19
+ function normalizeLink(link) {
20
+ const parts = [];
21
+ parts.push(`- [${link.title}](${link.href})`);
22
+ if (link.description) {
23
+ parts.push(` ${link.description}`);
24
+ }
25
+ return parts.join("\n");
26
+ }
27
+ function normalizeSection(section) {
28
+ const parts = [];
29
+ parts.push(`## ${section.title}`);
30
+ parts.push("");
31
+ if (section.description) {
32
+ const descriptions = Array.isArray(section.description) ? section.description : [section.description];
33
+ parts.push(...descriptions);
34
+ parts.push("");
35
+ }
36
+ if (section.links?.length) {
37
+ parts.push(...section.links.map(normalizeLink));
38
+ }
39
+ return parts.join("\n");
40
+ }
41
+ function normalizeLlmsTxtConfig(config) {
42
+ const parts = [];
43
+ if (config.sections?.length) {
44
+ parts.push(...config.sections.map(normalizeSection));
45
+ }
46
+ if (config.notes) {
47
+ parts.push("## Notes");
48
+ parts.push("");
49
+ const notes = Array.isArray(config.notes) ? config.notes : [config.notes];
50
+ parts.push(...notes);
51
+ }
52
+ return parts.join("\n\n");
53
+ }
54
+
55
+ const logger = consola.withTag("nuxt-mdream");
56
+ function setupPrerenderHandler() {
57
+ const nuxt = useNuxt();
58
+ nuxt.options.aiReady || {};
59
+ const pages = [];
60
+ nuxt.hooks.hook("nitro:init", async (nitro) => {
61
+ nitro.hooks.hook("prerender:generate", async (route) => {
62
+ if (!route.fileName?.endsWith(".md")) {
63
+ return;
64
+ }
65
+ const { markdown, title, description } = JSON.parse(route.contents || "{}");
66
+ const page = {
67
+ filePath: route.fileName,
68
+ url: route.route,
69
+ title,
70
+ content: markdown,
71
+ metadata: {
72
+ description,
73
+ title
74
+ }
75
+ };
76
+ pages.push(page);
77
+ route.contents = markdown;
78
+ });
79
+ nitro.hooks.hook("prerender:done", async () => {
80
+ if (pages.length === 0) {
81
+ return;
82
+ }
83
+ const startTime = Date.now();
84
+ const siteConfig = useSiteConfig();
85
+ const artifacts = await generateLlmsTxtArtifacts({
86
+ origin: siteConfig.url,
87
+ files: pages,
88
+ generateFull: true,
89
+ siteName: siteConfig.name || siteConfig.url,
90
+ description: siteConfig.description
91
+ });
92
+ logger.success(`Generated markdown for ${pages.length} pages`);
93
+ const hookPayload = {
94
+ content: artifacts.llmsTxt || "",
95
+ fullContent: artifacts.llmsFullTxt || "",
96
+ pages
97
+ };
98
+ const llmsTxtConfig = nuxt.options.runtimeConfig["nuxt-ai-ready"].llmsTxt;
99
+ const normalizedContent = normalizeLlmsTxtConfig(llmsTxtConfig);
100
+ if (normalizedContent) {
101
+ hookPayload.content = `${hookPayload.content}
102
+
103
+ ${normalizedContent}
104
+ `;
105
+ }
106
+ await nuxt.hooks.callHook("ai-ready:llms-txt", hookPayload);
107
+ const finalLlmsTxt = hookPayload.content;
108
+ const finalLlmsFullTxt = hookPayload.fullContent;
109
+ const generatedFiles = [];
110
+ if (finalLlmsTxt) {
111
+ const llmsTxtPath = join(nitro.options.output.publicDir, "llms.txt");
112
+ await writeFile(llmsTxtPath, finalLlmsTxt, "utf-8");
113
+ const sizeKb = (Buffer.byteLength(finalLlmsTxt, "utf-8") / 1024).toFixed(2);
114
+ generatedFiles.push({ path: "llms.txt", size: `${sizeKb}kb` });
115
+ nitro._prerenderedRoutes.push({
116
+ route: "/llms.txt",
117
+ fileName: llmsTxtPath,
118
+ generateTimeMS: 0
119
+ });
120
+ }
121
+ if (finalLlmsFullTxt) {
122
+ const llmsFullTxtPath = join(nitro.options.output.publicDir, "llms-full.txt");
123
+ await writeFile(llmsFullTxtPath, finalLlmsFullTxt, "utf-8");
124
+ const sizeKb = (Buffer.byteLength(finalLlmsFullTxt, "utf-8") / 1024).toFixed(2);
125
+ generatedFiles.push({ path: "llms-full.txt", size: `${sizeKb}kb` });
126
+ nitro._prerenderedRoutes.push({
127
+ route: "/llms-full.txt",
128
+ fileName: llmsFullTxtPath,
129
+ generateTimeMS: 0
130
+ });
131
+ }
132
+ if (generatedFiles.length > 0) {
133
+ const elapsed = Date.now() - startTime;
134
+ const fileList = generatedFiles.map((f) => `${f.path} (${f.size})`).join(" and ");
135
+ logger.info(`Generated ${fileList} in ${elapsed}ms`);
136
+ }
137
+ });
138
+ });
139
+ }
140
+
141
+ function generateVectorId(route, chunkIdx) {
142
+ const hash = createHash("sha256").update(route).digest("hex").substring(0, 48);
143
+ return `${hash}-${chunkIdx}`;
144
+ }
145
+ const module = defineNuxtModule({
146
+ meta: {
147
+ name: "nuxt-ai-ready",
148
+ compatibility: {
149
+ nuxt: ">=4.0.0"
150
+ },
151
+ configKey: "aiReady"
152
+ },
153
+ moduleDependencies: {
154
+ "@nuxtjs/robots": {
155
+ version: ">=5.6.0",
156
+ defaults: {
157
+ groups: [
158
+ {
159
+ userAgent: "*",
160
+ contentUsage: ["train-ai=y"],
161
+ contentSignal: ["ai-train=yes", "search=yes", "ai-input=yes"]
162
+ }
163
+ ]
164
+ }
165
+ },
166
+ "nuxt-site-config": {
167
+ version: ">=3"
168
+ },
169
+ "@nuxtjs/mcp-toolkit": {
170
+ version: ">=0.4.0",
171
+ optional: true
172
+ }
173
+ },
174
+ defaults() {
175
+ return {
176
+ enabled: true,
177
+ debug: false,
178
+ bulkRoute: "/content.jsonl",
179
+ mdreamOptions: {
180
+ preset: "minimal"
181
+ },
182
+ markdownCacheHeaders: {
183
+ maxAge: 3600,
184
+ // 1 hour
185
+ swr: true
186
+ }
187
+ };
188
+ },
189
+ async setup(config, nuxt) {
190
+ const { resolve: resolve$1 } = createResolver(import.meta.url);
191
+ const { version } = await readPackageJSON(resolve$1("../package.json"));
192
+ logger$1.level = config.debug || nuxt.options.debug ? 4 : 3;
193
+ if (config.enabled === false) {
194
+ logger$1.debug("Module is disabled, skipping setup.");
195
+ return;
196
+ }
197
+ await installNuxtSiteConfig();
198
+ nuxt.options.nitro.alias = nuxt.options.nitro.alias || {};
199
+ nuxt.options.alias["#ai-ready"] = resolve$1("./runtime");
200
+ if (!nuxt.options.mcp?.name) {
201
+ nuxt.options.mcp = nuxt.options.mcp || {};
202
+ nuxt.options.mcp.name = useSiteConfig().name;
203
+ }
204
+ nuxt.options.nitro.scanDirs = nuxt.options.nitro.scanDirs || [];
205
+ nuxt.options.nitro.scanDirs.push(
206
+ resolve$1("./runtime/server/utils"),
207
+ resolve$1("./runtime/server/mcp")
208
+ );
209
+ addTypeTemplate({
210
+ filename: "module/nuxt-ai-ready.d.ts",
211
+ getContents: (data) => {
212
+ const typesPath = relative(resolve$1(data.nuxt.options.rootDir, data.nuxt.options.buildDir, "module"), resolve$1("runtime/types"));
213
+ const nitroTypes = ` interface NitroRuntimeHooks {
214
+ 'ai-ready:markdown': (context: import('${typesPath}').MarkdownContext) => void | Promise<void>
215
+ 'ai-ready:mdreamConfig': (config: import('mdream').HTMLToMarkdownOptions) => void | Promise<void>
216
+ }`;
217
+ return `// Generated by nuxt-ai-ready
218
+ declare module 'nitropack/types' {
219
+ ${nitroTypes}
220
+ }
221
+
222
+ declare module 'nitropack' {
223
+ ${nitroTypes}
224
+ }
225
+
226
+ export {}
227
+ `;
228
+ }
229
+ }, {
230
+ nitro: true
231
+ });
232
+ const defaultLlmsTxtSections = [];
233
+ if (config.bulkRoute !== false) {
234
+ const resolvedBulkRoute = withSiteUrl(config.bulkRoute);
235
+ defaultLlmsTxtSections.push({
236
+ title: "AI Tools",
237
+ links: [
238
+ {
239
+ title: "Bulk Data",
240
+ href: resolvedBulkRoute,
241
+ description: `\`\`\`bash
242
+ curl "${resolvedBulkRoute}"
243
+ \`\`\`
244
+
245
+ Returns JSONL (newline-delimited JSON) with all indexed content.`
246
+ }
247
+ ]
248
+ });
249
+ }
250
+ const hasMCP = hasNuxtModule("@nuxtjs/mcp-toolkit");
251
+ if (hasMCP) {
252
+ nuxt.hook("mcp:definitions:paths", (paths) => {
253
+ const mcpRuntimeDir = resolve$1("./runtime/server/mcp");
254
+ paths.tools = paths.tools || [];
255
+ paths.resources = paths.resources || [];
256
+ paths.prompts = paths.prompts || [];
257
+ paths.tools.push(`${mcpRuntimeDir}/tools`);
258
+ paths.resources.push(`${mcpRuntimeDir}/resources`);
259
+ paths.prompts.push(`${mcpRuntimeDir}/prompts`);
260
+ });
261
+ const mcpLink = {
262
+ title: "MCP",
263
+ href: withSiteUrl(nuxt.options.mcp?.route || "/mcp")
264
+ };
265
+ if (config.bulkRoute !== false && defaultLlmsTxtSections[0]) {
266
+ defaultLlmsTxtSections[0].links.push(mcpLink);
267
+ } else {
268
+ defaultLlmsTxtSections.push({
269
+ title: "AI Tools - API Endpoints",
270
+ links: [mcpLink]
271
+ });
272
+ }
273
+ }
274
+ const mergedLlmsTxt = config.llmsTxt ? {
275
+ sections: [
276
+ ...defaultLlmsTxtSections,
277
+ ...config.llmsTxt.sections || []
278
+ ],
279
+ notes: config.llmsTxt.notes
280
+ } : { sections: defaultLlmsTxtSections };
281
+ nuxt.options.runtimeConfig["nuxt-ai-ready"] = {
282
+ version: version || "0.0.0",
283
+ debug: config.debug || false,
284
+ bulkRoute: config.bulkRoute,
285
+ mdreamOptions: config.mdreamOptions || {},
286
+ markdownCacheHeaders: defu(config.markdownCacheHeaders, {
287
+ maxAge: 3600,
288
+ swr: true
289
+ }),
290
+ llmsTxt: mergedLlmsTxt
291
+ };
292
+ addServerHandler({
293
+ middleware: true,
294
+ handler: resolve$1("./runtime/server/middleware/mdream")
295
+ });
296
+ if (nuxt.options.build) {
297
+ addPlugin({ mode: "server", src: resolve$1("./runtime/nuxt/plugins/prerender") });
298
+ }
299
+ if (nuxt.options.dev) {
300
+ addServerHandler({ route: "/llms.txt", handler: resolve$1("./runtime/server/routes/llms.txt.get") });
301
+ }
302
+ const isStatic = nuxt.options.nitro.static || nuxt.options._generate || false;
303
+ if (isStatic || nuxt.options.nitro.prerender?.routes?.length) {
304
+ setupPrerenderHandler();
305
+ }
306
+ if (config.bulkRoute !== false) {
307
+ nuxt.options.nitro.routeRules = nuxt.options.nitro.routeRules || {};
308
+ nuxt.options.nitro.routeRules[config.bulkRoute] = {
309
+ headers: {
310
+ "Content-Type": "application/x-ndjson; charset=utf-8"
311
+ }
312
+ };
313
+ }
314
+ const isBuildMode = !nuxt.options._prepare && !nuxt.options.dev;
315
+ nuxt.hooks.hook("modules:done", () => {
316
+ nuxt.hook("nitro:init", async (nitro) => {
317
+ if (!isBuildMode) {
318
+ logger$1.debug("Dev mode: skipping llms.txt generation");
319
+ return;
320
+ }
321
+ if (config.bulkRoute === false) {
322
+ logger$1.debug("Bulk route disabled, skipping bulk generation");
323
+ return;
324
+ }
325
+ const bulkPath = resolve(nitro.options.output.dir, `public${config.bulkRoute}`);
326
+ let bulkStream = null;
327
+ let bulkStreamEntries = 0;
328
+ nitro.hooks.hook("prerender:route", async (route) => {
329
+ const isHtml = route.fileName?.endsWith(".html") && route.contents.startsWith("<!DOCTYPE html");
330
+ if (!isHtml || !route._sitemap || !route.contents) {
331
+ return;
332
+ }
333
+ let title = "";
334
+ let description = "";
335
+ const headings = [];
336
+ const extractPlugin = extractionPlugin({
337
+ title(el) {
338
+ title = el.textContent;
339
+ },
340
+ 'meta[name="description"]': (el) => {
341
+ description = el.attributes.content || "";
342
+ },
343
+ "h1, h2, h3, h4, h5, h6": (el) => {
344
+ const text = el.textContent?.trim();
345
+ const level = el.name.toLowerCase();
346
+ if (text)
347
+ headings.push({ [level]: text });
348
+ }
349
+ });
350
+ const options = {
351
+ origin: useSiteConfig().url,
352
+ ...config.mdreamOptions || {}
353
+ };
354
+ options.plugins = [extractPlugin, ...options.plugins || []];
355
+ const chunksStream = htmlToMarkdownSplitChunksStream(route.contents, {
356
+ ...options,
357
+ headersToSplitOn: [TagIdMap.h1, TagIdMap.h2, TagIdMap.h3],
358
+ origin: useSiteConfig().url,
359
+ chunkSize: 256,
360
+ stripHeaders: false,
361
+ lengthFunction(text) {
362
+ return estimateTokenCount(text);
363
+ }
364
+ });
365
+ if (!bulkStream) {
366
+ mkdirSync(dirname(bulkPath), { recursive: true });
367
+ bulkStream = createWriteStream(bulkPath);
368
+ logger$1.info(`Bulk JSONL stream created at ${relative(nuxt.options.rootDir, bulkPath)}`);
369
+ }
370
+ let idx = 0;
371
+ for (const chunk of chunksStream) {
372
+ const bulkChunk = {
373
+ id: generateVectorId(route.route, idx),
374
+ route: route.route,
375
+ chunkIndex: idx,
376
+ content: chunk.content,
377
+ headers: chunk.metadata?.headers,
378
+ loc: chunk.metadata?.loc,
379
+ title,
380
+ description
381
+ };
382
+ await nuxt.hooks.callHook("ai-ready:chunk", {
383
+ chunk: bulkChunk,
384
+ route: route.route,
385
+ title,
386
+ description,
387
+ headings
388
+ });
389
+ bulkStream.write(`${JSON.stringify(bulkChunk)}
390
+ `);
391
+ bulkStreamEntries++;
392
+ idx++;
393
+ }
394
+ });
395
+ nitro.hooks.hook("prerender:done", () => {
396
+ if (bulkStream) {
397
+ bulkStream.end();
398
+ logger$1.success(`Bulk JSONL exported ${bulkStreamEntries} entries.`);
399
+ }
400
+ });
401
+ });
402
+ });
403
+ }
404
+ });
405
+
406
+ export { module as default };
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import { defineNuxtPlugin, prerenderRoutes } from "nuxt/app";
2
+ export default defineNuxtPlugin({
3
+ setup(nuxtApp) {
4
+ if (!import.meta.prerender) {
5
+ return;
6
+ }
7
+ nuxtApp.hooks.hook("app:rendered", (ctx) => {
8
+ let url = ctx.ssrContext?.url || "";
9
+ if (url.endsWith(".md")) {
10
+ return;
11
+ }
12
+ if (url.endsWith("/")) {
13
+ url = `${url}index.md`;
14
+ } else {
15
+ url = `${url}.md`;
16
+ }
17
+ prerenderRoutes(url);
18
+ });
19
+ }
20
+ });
@@ -0,0 +1 @@
1
+ export declare const logger: import("consola").ConsolaInstance;
@@ -0,0 +1,4 @@
1
+ import { createConsola } from "consola";
2
+ export const logger = createConsola({
3
+ defaults: { tag: "nuxt-ai-ready" }
4
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,62 @@
1
+ import { defineMcpPrompt } from "#imports";
2
+ import { streamBulkDocuments } from "../../utils/db.js";
3
+ export default defineMcpPrompt({
4
+ name: "explain_concept",
5
+ description: "Get a detailed explanation of a concept by finding and reading relevant pages",
6
+ arguments: [
7
+ {
8
+ name: "concept",
9
+ description: "The concept or feature to explain",
10
+ required: true
11
+ },
12
+ {
13
+ name: "level",
14
+ description: "Explanation level: beginner, intermediate, or advanced",
15
+ required: false
16
+ }
17
+ ],
18
+ handler: async ({ concept, level = "intermediate" }) => {
19
+ const searchLower = concept.toLowerCase();
20
+ const seenRoutes = /* @__PURE__ */ new Set();
21
+ const relevantPages = [];
22
+ for await (const doc of streamBulkDocuments()) {
23
+ if (seenRoutes.has(doc.route))
24
+ continue;
25
+ const matches = doc.title?.toLowerCase().includes(searchLower) || doc.description?.toLowerCase().includes(searchLower) || doc.route?.toLowerCase().includes(searchLower);
26
+ if (matches) {
27
+ seenRoutes.add(doc.route);
28
+ relevantPages.push({
29
+ route: doc.route,
30
+ title: doc.title,
31
+ description: doc.description
32
+ });
33
+ if (relevantPages.length >= 10)
34
+ break;
35
+ }
36
+ }
37
+ return {
38
+ messages: [
39
+ {
40
+ role: "user",
41
+ content: {
42
+ type: "text",
43
+ text: `Please explain "${concept}" at a ${level} level.
44
+
45
+ Here are the relevant pages found: ${JSON.stringify(relevantPages, null, 2)}
46
+
47
+ Please:
48
+ 1. Use get_page to read the most relevant pages (top 2-3)
49
+ 2. Synthesize the information to create an explanation that:
50
+ - Provides clear definitions
51
+ - Includes practical examples from the pages
52
+ - Explains use cases
53
+ - Mentions related concepts
54
+ - References the specific pages used
55
+
56
+ Tailor the explanation for a ${level} audience.`
57
+ }
58
+ }
59
+ ]
60
+ };
61
+ }
62
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;