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.
- package/LICENSE.md +9 -0
- package/README.md +63 -0
- package/dist/module.d.mts +62 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +406 -0
- package/dist/runtime/nuxt/plugins/prerender.d.ts +2 -0
- package/dist/runtime/nuxt/plugins/prerender.js +20 -0
- package/dist/runtime/server/logger.d.ts +1 -0
- package/dist/runtime/server/logger.js +4 -0
- package/dist/runtime/server/mcp/prompts/explain-concept.d.ts +2 -0
- package/dist/runtime/server/mcp/prompts/explain-concept.js +62 -0
- package/dist/runtime/server/mcp/prompts/find-information.d.ts +2 -0
- package/dist/runtime/server/mcp/prompts/find-information.js +57 -0
- package/dist/runtime/server/mcp/prompts/search-content.d.ts +2 -0
- package/dist/runtime/server/mcp/prompts/search-content.js +58 -0
- package/dist/runtime/server/mcp/resources/all-content.d.ts +2 -0
- package/dist/runtime/server/mcp/resources/all-content.js +14 -0
- package/dist/runtime/server/mcp/resources/pages.d.ts +2 -0
- package/dist/runtime/server/mcp/resources/pages.js +23 -0
- package/dist/runtime/server/mcp/tools/get-page.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/get-page.js +42 -0
- package/dist/runtime/server/mcp/tools/list-pages.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/list-pages.js +78 -0
- package/dist/runtime/server/middleware/mdream.d.ts +2 -0
- package/dist/runtime/server/middleware/mdream.js +132 -0
- package/dist/runtime/server/routes/llms.txt.get.d.ts +2 -0
- package/dist/runtime/server/routes/llms.txt.get.js +23 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/db.d.ts +8 -0
- package/dist/runtime/server/utils/db.js +48 -0
- package/dist/runtime/types.d.ts +166 -0
- package/dist/runtime/types.js +0 -0
- package/dist/types.d.mts +12 -0
- 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 };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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,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,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
|
+
});
|