@bdocs/plugin-llms-text 0.2.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 +21 -0
- package/dist/node/index.d.mts +46 -0
- package/dist/node/index.mjs +359 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Boltdocs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BoltdocsPlugin } from "boltdocs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/node/schema.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Controls which routes are included in the llms.txt.
|
|
7
|
+
*/
|
|
8
|
+
declare const LlmsTextPluginOptionsSchema: z.ZodObject<{
|
|
9
|
+
title: z.ZodOptional<z.ZodString>;
|
|
10
|
+
description: z.ZodOptional<z.ZodString>;
|
|
11
|
+
bodyText: z.ZodOptional<z.ZodString>;
|
|
12
|
+
includePaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
13
|
+
excludePaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
|
+
locales: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
|
+
sections: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
16
|
+
title: z.ZodString;
|
|
17
|
+
pathPrefix: z.ZodString;
|
|
18
|
+
description: z.ZodOptional<z.ZodString>;
|
|
19
|
+
maxLinks: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
optional: z.ZodDefault<z.ZodBoolean>;
|
|
21
|
+
}, z.core.$strip>>>;
|
|
22
|
+
sortBy: z.ZodDefault<z.ZodEnum<{
|
|
23
|
+
title: "title";
|
|
24
|
+
path: "path";
|
|
25
|
+
sidebarPosition: "sidebarPosition";
|
|
26
|
+
}>>;
|
|
27
|
+
maxLinksPerSection: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
includeDrafts: z.ZodDefault<z.ZodBoolean>;
|
|
29
|
+
devMode: z.ZodDefault<z.ZodBoolean>;
|
|
30
|
+
addLinkTag: z.ZodDefault<z.ZodBoolean>;
|
|
31
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
type LlmsTextPluginOptions = z.input<typeof LlmsTextPluginOptionsSchema>;
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/node/index.d.ts
|
|
36
|
+
/**
|
|
37
|
+
* @bdocs/plugin-llms-text — Generate an `llms.txt` file at build time.
|
|
38
|
+
*
|
|
39
|
+
* The llms.txt specification (llmstxt.org) provides a standardised
|
|
40
|
+
* plain-text index of documentation pages optimised for Large Language
|
|
41
|
+
* Models and AI agents. The file is emitted into the resolved build output
|
|
42
|
+
* directory and is therefore served at `<siteUrl>/llms.txt`.
|
|
43
|
+
*/
|
|
44
|
+
declare function llmsTextPlugin(rawOptions?: LlmsTextPluginOptions): BoltdocsPlugin;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { type LlmsTextPluginOptions, llmsTextPlugin as default };
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { createPlugin } from "boltdocs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
//#region src/node/schema.ts
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for a custom section in the llms.txt file.
|
|
8
|
+
* Each section maps to an H2 heading with a list of curated links.
|
|
9
|
+
*/
|
|
10
|
+
const LlmsTextSectionSchema = z.object({
|
|
11
|
+
/** The H2 heading label for this section (e.g. 'Getting Started'). */
|
|
12
|
+
title: z.string().min(1).max(100),
|
|
13
|
+
/** Path prefix filter — only routes starting with this path are included. */
|
|
14
|
+
pathPrefix: z.string().min(1).max(200),
|
|
15
|
+
/** Optional description paragraph placed right after the H2 heading. */
|
|
16
|
+
description: z.string().max(500).optional(),
|
|
17
|
+
/** Maximum links in this section. Default: no limit. */
|
|
18
|
+
maxLinks: z.number().int().positive().max(500).optional(),
|
|
19
|
+
/** Whether this section should appear under the '## Optional' umbrella. */
|
|
20
|
+
optional: z.boolean().default(false)
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Controls link sorting within each section.
|
|
24
|
+
*/
|
|
25
|
+
const SortBySchema = z.enum([
|
|
26
|
+
"path",
|
|
27
|
+
"title",
|
|
28
|
+
"sidebarPosition"
|
|
29
|
+
]).default("sidebarPosition");
|
|
30
|
+
/**
|
|
31
|
+
* Controls which routes are included in the llms.txt.
|
|
32
|
+
*/
|
|
33
|
+
const LlmsTextPluginOptionsSchema = z.object({
|
|
34
|
+
/**
|
|
35
|
+
* Project title used as the H1 heading.
|
|
36
|
+
* Defaults to the site title from boltdocs config.
|
|
37
|
+
*/
|
|
38
|
+
title: z.string().min(1).max(200).optional(),
|
|
39
|
+
/**
|
|
40
|
+
* Project description used as the blockquote summary.
|
|
41
|
+
* Defaults to the site description from boltdocs config.
|
|
42
|
+
*/
|
|
43
|
+
description: z.string().min(1).max(1e3).optional(),
|
|
44
|
+
/**
|
|
45
|
+
* Additional markdown body text inserted after the blockquote
|
|
46
|
+
* and before any H2 sections. Use this for LLM-specific instructions,
|
|
47
|
+
* common patterns, or high-level architecture notes.
|
|
48
|
+
*/
|
|
49
|
+
bodyText: z.string().max(2e3).optional(),
|
|
50
|
+
/**
|
|
51
|
+
* If set, only routes matching at least one of these path prefixes
|
|
52
|
+
* are included. Example: ['/docs', '/blog']. Default: all routes.
|
|
53
|
+
*/
|
|
54
|
+
includePaths: z.array(z.string()).optional(),
|
|
55
|
+
/**
|
|
56
|
+
* Routes matching any of these path prefixes are excluded.
|
|
57
|
+
* Applied AFTER includePaths. Example: ['/docs/api/experimental'].
|
|
58
|
+
*/
|
|
59
|
+
excludePaths: z.array(z.string()).optional(),
|
|
60
|
+
/**
|
|
61
|
+
* Restrict links to the selected locale codes. The default locale is
|
|
62
|
+
* matched automatically even when its RouteMeta.locale is undefined.
|
|
63
|
+
* Example: ['en', 'es']. When omitted, all locales are included.
|
|
64
|
+
*/
|
|
65
|
+
locales: z.array(z.string().trim().min(1)).optional(),
|
|
66
|
+
/**
|
|
67
|
+
* Custom H2 sections that group links by path prefix.
|
|
68
|
+
* When provided, the default "Documentation" section is replaced.
|
|
69
|
+
* Default: a single "Documentation" section with all routes.
|
|
70
|
+
*/
|
|
71
|
+
sections: z.array(LlmsTextSectionSchema).optional(),
|
|
72
|
+
/**
|
|
73
|
+
* How to sort links within each section.
|
|
74
|
+
*/
|
|
75
|
+
sortBy: SortBySchema,
|
|
76
|
+
/**
|
|
77
|
+
* Maximum number of links per section. Default: no limit.
|
|
78
|
+
*/
|
|
79
|
+
maxLinksPerSection: z.number().int().positive().max(500).optional(),
|
|
80
|
+
/**
|
|
81
|
+
* Whether to include draft routes. Default: false.
|
|
82
|
+
*/
|
|
83
|
+
includeDrafts: z.boolean().default(false),
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated The afterBuild hook is always registered now; generation
|
|
86
|
+
* happens on every production build. This option is kept for backwards
|
|
87
|
+
* compatibility and no longer has any effect.
|
|
88
|
+
*/
|
|
89
|
+
devMode: z.boolean().default(false),
|
|
90
|
+
/**
|
|
91
|
+
* Whether to inject a `<link rel="llms-txt">` tag into the HTML `<head>`.
|
|
92
|
+
* Default: true.
|
|
93
|
+
*/
|
|
94
|
+
addLinkTag: z.boolean().default(true),
|
|
95
|
+
/**
|
|
96
|
+
* URL base for generating absolute links in llms.txt.
|
|
97
|
+
* Falls back to `siteUrl` from the boltdocs config.
|
|
98
|
+
*/
|
|
99
|
+
baseUrl: z.string().url().optional()
|
|
100
|
+
});
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/node/generator.ts
|
|
103
|
+
/**
|
|
104
|
+
* Sort routes within a section according to the configured sort order.
|
|
105
|
+
*/
|
|
106
|
+
function sortRoutes(routes, sortBy) {
|
|
107
|
+
const sorted = [...routes];
|
|
108
|
+
switch (sortBy) {
|
|
109
|
+
case "path":
|
|
110
|
+
sorted.sort((a, b) => a.path.localeCompare(b.path));
|
|
111
|
+
break;
|
|
112
|
+
case "title":
|
|
113
|
+
sorted.sort((a, b) => a.title.localeCompare(b.title));
|
|
114
|
+
break;
|
|
115
|
+
case "sidebarPosition":
|
|
116
|
+
sorted.sort((a, b) => {
|
|
117
|
+
const aPos = a.sidebarPosition ?? 999;
|
|
118
|
+
const bPos = b.sidebarPosition ?? 999;
|
|
119
|
+
if (aPos !== bPos) return aPos - bPos;
|
|
120
|
+
return a.title.localeCompare(b.title);
|
|
121
|
+
});
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return sorted;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Format a single link line following the llms.txt spec:
|
|
128
|
+
* `- [Page Title](https://site.com/docs/page): Brief description`
|
|
129
|
+
*/
|
|
130
|
+
function formatLink(route, siteUrl) {
|
|
131
|
+
const url = `${siteUrl.replace(/\/+$/, "")}${route.path}`;
|
|
132
|
+
const description = route.excerpt ?? route.description ?? "";
|
|
133
|
+
const label = description ? `: ${description.replace(/\n/g, " ").replace(/\s+/g, " ").trim()}` : "";
|
|
134
|
+
return `- [${escapeLinkText(route.title)}](${url})${label}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Escape special characters in plain-text Markdown link text.
|
|
138
|
+
* Square brackets would break the `[text]` syntax, and parentheses
|
|
139
|
+
* would break the `(url)` syntax.
|
|
140
|
+
*/
|
|
141
|
+
function escapeLinkText(text) {
|
|
142
|
+
return text.replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check a single route against include/exclude path filters.
|
|
146
|
+
* Returns `true` when the route passes all filters.
|
|
147
|
+
*/
|
|
148
|
+
function routePassesPathFilter(route, config) {
|
|
149
|
+
if (config.locales && config.locales.length > 0) {
|
|
150
|
+
const routeLocale = route.locale ?? config.defaultLocale;
|
|
151
|
+
if (routeLocale && !config.locales.includes(routeLocale)) return false;
|
|
152
|
+
}
|
|
153
|
+
if (config.includePaths && config.includePaths.length > 0) {
|
|
154
|
+
if (!config.includePaths.some((p) => route.path.startsWith(p))) return false;
|
|
155
|
+
}
|
|
156
|
+
if (config.excludePaths && config.excludePaths.length > 0) {
|
|
157
|
+
if (config.excludePaths.some((p) => route.path.startsWith(p))) return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Group routes into a single section based on path prefix matching.
|
|
163
|
+
*/
|
|
164
|
+
function routesForSection(routes, section, config, skipSort = false) {
|
|
165
|
+
const filtered = routes.filter((r) => {
|
|
166
|
+
if (!config.includeDrafts && r.draft) return false;
|
|
167
|
+
if (!routePassesPathFilter(r, config)) return false;
|
|
168
|
+
return r.path.startsWith(section.pathPrefix);
|
|
169
|
+
});
|
|
170
|
+
const result = skipSort ? filtered : sortRoutes(filtered, config.sortBy);
|
|
171
|
+
const maxLinks = section.maxLinks ?? config.maxLinksPerSection;
|
|
172
|
+
if (maxLinks && result.length > maxLinks) return result.slice(0, maxLinks);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Generate the full llms.txt Markdown content.
|
|
177
|
+
*
|
|
178
|
+
* Follows the llms.txt specification:
|
|
179
|
+
* 1. H1 - Project title
|
|
180
|
+
* 2. Blockquote - Summary/short description
|
|
181
|
+
* 3. Optional body text
|
|
182
|
+
* 4. H2 sections with curated links
|
|
183
|
+
* 5. Optional sections under `## Optional`
|
|
184
|
+
*/
|
|
185
|
+
function generateLlmsText(routes, config) {
|
|
186
|
+
const lines = [];
|
|
187
|
+
lines.push(`# ${config.title}`);
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push(`> ${config.description}`);
|
|
190
|
+
lines.push("");
|
|
191
|
+
if (config.bodyText) {
|
|
192
|
+
lines.push(config.bodyText.trim());
|
|
193
|
+
lines.push("");
|
|
194
|
+
}
|
|
195
|
+
const requiredSections = config.sections.filter((s) => !s.optional);
|
|
196
|
+
const optionalSections = config.sections.filter((s) => s.optional);
|
|
197
|
+
for (const section of requiredSections) {
|
|
198
|
+
const sectionRoutes = routesForSection(routes, section, config);
|
|
199
|
+
if (sectionRoutes.length === 0) continue;
|
|
200
|
+
lines.push(`## ${section.title}`);
|
|
201
|
+
if (section.description) {
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push(section.description);
|
|
204
|
+
}
|
|
205
|
+
lines.push("");
|
|
206
|
+
for (const route of sectionRoutes) lines.push(formatLink(route, config.siteUrl));
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
if (optionalSections.length > 0) {
|
|
210
|
+
const allOptionalRoutes = [];
|
|
211
|
+
for (const section of optionalSections) {
|
|
212
|
+
const sectionRoutes = routesForSection(routes, section, config, true);
|
|
213
|
+
allOptionalRoutes.push(...sectionRoutes);
|
|
214
|
+
}
|
|
215
|
+
if (allOptionalRoutes.length > 0) {
|
|
216
|
+
const sorted = sortRoutes(allOptionalRoutes, config.sortBy);
|
|
217
|
+
lines.push("## Optional");
|
|
218
|
+
lines.push("");
|
|
219
|
+
for (const route of sorted) lines.push(formatLink(route, config.siteUrl));
|
|
220
|
+
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return lines.join("\n").trim() + "\n";
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Build the default sections from available routes when the user
|
|
227
|
+
* has not provided custom sections.
|
|
228
|
+
*/
|
|
229
|
+
function buildDefaultSections(routes) {
|
|
230
|
+
const sections = [];
|
|
231
|
+
const collectionPrefixes = /* @__PURE__ */ new Set();
|
|
232
|
+
for (const route of routes) {
|
|
233
|
+
if (route.draft) continue;
|
|
234
|
+
const parts = route.path.split("/").filter(Boolean);
|
|
235
|
+
if (parts.length > 1) {
|
|
236
|
+
const prefix = "/" + parts[0] + "/";
|
|
237
|
+
if ([
|
|
238
|
+
"/blog/",
|
|
239
|
+
"/changelog/",
|
|
240
|
+
"/news/",
|
|
241
|
+
"/release-notes/"
|
|
242
|
+
].some((p) => prefix.startsWith(p))) collectionPrefixes.add(prefix);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (routes.filter((r) => {
|
|
246
|
+
if (r.draft) return false;
|
|
247
|
+
if (r.path.split("/").filter(Boolean).length <= 1) return true;
|
|
248
|
+
for (const cp of collectionPrefixes) if (r.path.startsWith(cp)) return false;
|
|
249
|
+
return true;
|
|
250
|
+
}).length > 0) sections.push({
|
|
251
|
+
title: "Documentation",
|
|
252
|
+
pathPrefix: "/",
|
|
253
|
+
description: "Core documentation pages covering installation, usage, API reference, and guides.",
|
|
254
|
+
optional: false
|
|
255
|
+
});
|
|
256
|
+
for (const prefix of collectionPrefixes) {
|
|
257
|
+
const label = prefix.replace(/^\//, "").replace(/\/$/, "").split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
258
|
+
sections.push({
|
|
259
|
+
title: label,
|
|
260
|
+
pathPrefix: prefix,
|
|
261
|
+
description: `${label} articles and posts.`,
|
|
262
|
+
optional: true
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return sections;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Write the generated llms.txt to disk.
|
|
269
|
+
*/
|
|
270
|
+
function writeLlmsText(content, outDir, logger) {
|
|
271
|
+
const outputPath = path.join(outDir, "llms.txt");
|
|
272
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
273
|
+
fs.writeFileSync(outputPath, content, "utf-8");
|
|
274
|
+
logger(`llms.txt generated: llms.txt (${Buffer.byteLength(content, "utf-8")} bytes)`);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Format the site URL — strip trailing slash for consistency.
|
|
278
|
+
*/
|
|
279
|
+
function formatSiteUrl(raw) {
|
|
280
|
+
return raw.replace(/\/+$/, "");
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/node/index.ts
|
|
284
|
+
/**
|
|
285
|
+
* @bdocs/plugin-llms-text — Generate an `llms.txt` file at build time.
|
|
286
|
+
*
|
|
287
|
+
* The llms.txt specification (llmstxt.org) provides a standardised
|
|
288
|
+
* plain-text index of documentation pages optimised for Large Language
|
|
289
|
+
* Models and AI agents. The file is emitted into the resolved build output
|
|
290
|
+
* directory and is therefore served at `<siteUrl>/llms.txt`.
|
|
291
|
+
*/
|
|
292
|
+
function llmsTextPlugin(rawOptions = {}) {
|
|
293
|
+
const options = LlmsTextPluginOptionsSchema.parse(rawOptions);
|
|
294
|
+
function linkTagInjection(ctx, params) {
|
|
295
|
+
if (!options.addLinkTag) return { html: params.html };
|
|
296
|
+
const siteUrl = ctx.config.siteUrl ?? options.baseUrl;
|
|
297
|
+
if (!siteUrl) return { html: params.html };
|
|
298
|
+
const tag = `<link rel="llms-txt" href="${formatSiteUrl(siteUrl)}/llms.txt"/>\n</head>`;
|
|
299
|
+
return { html: params.html.replace("</head>", tag) };
|
|
300
|
+
}
|
|
301
|
+
function resolveConfig(ctx) {
|
|
302
|
+
const siteUrl = options.baseUrl ?? ctx.config.siteUrl;
|
|
303
|
+
if (!siteUrl) {
|
|
304
|
+
ctx.logger.info("[llms-text] Skipping generation: no siteUrl configured. Set siteUrl in boltdocs.config.ts or pass baseUrl to the plugin.");
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const themeTitle = ctx.config.theme?.title;
|
|
308
|
+
const title = options.title ?? (typeof themeTitle === "object" && themeTitle !== null ? Object.values(themeTitle)[0] : themeTitle) ?? "Documentation";
|
|
309
|
+
const themeDescription = ctx.config.theme?.description;
|
|
310
|
+
const description = options.description ?? (typeof themeDescription === "object" && themeDescription !== null ? Object.values(themeDescription)[0] : themeDescription) ?? "";
|
|
311
|
+
const sections = options.sections ?? buildDefaultSections(ctx.routes);
|
|
312
|
+
return {
|
|
313
|
+
title,
|
|
314
|
+
description,
|
|
315
|
+
bodyText: options.bodyText,
|
|
316
|
+
siteUrl: formatSiteUrl(siteUrl),
|
|
317
|
+
sections,
|
|
318
|
+
sortBy: options.sortBy,
|
|
319
|
+
maxLinksPerSection: options.maxLinksPerSection,
|
|
320
|
+
includeDrafts: options.includeDrafts,
|
|
321
|
+
includePaths: options.includePaths,
|
|
322
|
+
excludePaths: options.excludePaths,
|
|
323
|
+
locales: options.locales,
|
|
324
|
+
defaultLocale: ctx.config.i18n?.defaultLocale
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
let generatedKey = null;
|
|
328
|
+
const generate = (ctx, routes, outputDir) => {
|
|
329
|
+
const resolved = resolveConfig({
|
|
330
|
+
...ctx,
|
|
331
|
+
routes
|
|
332
|
+
});
|
|
333
|
+
if (!resolved) return;
|
|
334
|
+
const content = generateLlmsText(routes, resolved);
|
|
335
|
+
const normalizedOutputDir = path.resolve(outputDir);
|
|
336
|
+
const key = `${normalizedOutputDir}\0${content}`;
|
|
337
|
+
if (generatedKey === key) return;
|
|
338
|
+
writeLlmsText(content, normalizedOutputDir, ctx.logger.info);
|
|
339
|
+
generatedKey = key;
|
|
340
|
+
};
|
|
341
|
+
return createPlugin({
|
|
342
|
+
name: "boltdocs-plugin-llms-text",
|
|
343
|
+
version: "0.1.0",
|
|
344
|
+
hooks: {
|
|
345
|
+
async "build:generate"(ctx, params) {
|
|
346
|
+
generate(ctx, params.routes, params.outDir);
|
|
347
|
+
},
|
|
348
|
+
async afterBuild(ctx) {
|
|
349
|
+
const outputDir = path.resolve(ctx.rootDir ?? process.cwd(), ctx.outDir);
|
|
350
|
+
generate(ctx, ctx.routes, outputDir);
|
|
351
|
+
},
|
|
352
|
+
transformHtml(ctx, params) {
|
|
353
|
+
return linkTagInjection(ctx, params);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
export { llmsTextPlugin as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bdocs/plugin-llms-text",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "llms.txt generator plugin for Boltdocs — provides AI-optimized documentation indexes for LLMs and AI agents",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/node/index.d.mts",
|
|
14
|
+
"import": "./dist/node/index.mjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"author": "Jesus Alcala",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"zod": "^4.3.6"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"boltdocs": "^3.3.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"tsdown": "^0.21.7",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vitest": "^3.2.4",
|
|
31
|
+
"boltdocs": "^3.3.0",
|
|
32
|
+
"tsdown-config": "1.0.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsdown --config-loader unrun",
|
|
36
|
+
"dev": "tsdown --watch --config-loader unrun",
|
|
37
|
+
"test": "vitest run"
|
|
38
|
+
}
|
|
39
|
+
}
|