@mandujs/core 0.25.3 → 0.27.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/package.json +2 -1
- package/src/bundler/dev.ts +8 -0
- package/src/client/spa-nav-helper.ts +82 -0
- package/src/content/collection.ts +322 -19
- package/src/content/index.ts +11 -2
- package/src/content/llms-txt.ts +98 -17
- package/src/content/sidebar.ts +446 -28
- package/src/generator/generate.ts +8 -0
- package/src/kitchen/api/routes-api.ts +8 -1
- package/src/router/fs-patterns.ts +26 -1
- package/src/router/fs-routes.ts +21 -1
- package/src/router/fs-scanner.ts +81 -0
- package/src/router/fs-types.ts +52 -1
- package/src/router/index.ts +2 -0
- package/src/routes/index.ts +74 -0
- package/src/routes/metadata-routes.ts +427 -0
- package/src/routes/types.ts +341 -0
- package/src/runtime/server.ts +113 -0
- package/src/runtime/ssr.ts +12 -0
- package/src/runtime/streaming-ssr.ts +16 -0
- package/src/spec/schema.ts +51 -1
package/src/content/llms-txt.ts
CHANGED
|
@@ -72,8 +72,19 @@ export interface GenerateLLMSTxtOptions {
|
|
|
72
72
|
* an absolute origin (e.g. `https://example.com`) to produce an
|
|
73
73
|
* outward-facing llms.txt that third-party crawlers can consume
|
|
74
74
|
* without resolving against the host.
|
|
75
|
+
*
|
|
76
|
+
* Alias for `baseUrl` — accepts either name so the API reads
|
|
77
|
+
* naturally whether the caller thinks in "site base path" or
|
|
78
|
+
* "absolute URL".
|
|
75
79
|
*/
|
|
76
80
|
basePath?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Alias for `basePath`. When both are provided, `baseUrl` wins
|
|
83
|
+
* (callers explicitly typing an absolute URL typically mean
|
|
84
|
+
* "use this verbatim"). Useful in docs-site configs that already
|
|
85
|
+
* expose `baseUrl` for their router / canonical URL helpers.
|
|
86
|
+
*/
|
|
87
|
+
baseUrl?: string;
|
|
77
88
|
/**
|
|
78
89
|
* When true, include each entry's body verbatim under its heading.
|
|
79
90
|
* This produces the `llms-full.txt` variant — significantly larger
|
|
@@ -92,6 +103,18 @@ export interface GenerateLLMSTxtOptions {
|
|
|
92
103
|
* omits the trailing `: {summary}` tail.
|
|
93
104
|
*/
|
|
94
105
|
getSummary?: (entry: CollectionEntry<unknown>) => string;
|
|
106
|
+
/**
|
|
107
|
+
* When true, emit a nested heading structure that groups entries
|
|
108
|
+
* by their first slug segment (the "category"). With `full: true`
|
|
109
|
+
* this produces an `llms-full.txt` that mirrors the docs sidebar
|
|
110
|
+
* layout — useful for LLM crawlers that consume the
|
|
111
|
+
* category-section convention.
|
|
112
|
+
*
|
|
113
|
+
* The category headings use `###` so they nest cleanly under the
|
|
114
|
+
* `##` collection heading. Entries without a slash in their slug
|
|
115
|
+
* are placed under an implicit "root" section.
|
|
116
|
+
*/
|
|
117
|
+
groupByCategory?: boolean;
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
/**
|
|
@@ -108,11 +131,13 @@ export async function generateLLMSTxt(
|
|
|
108
131
|
const {
|
|
109
132
|
siteName,
|
|
110
133
|
description,
|
|
111
|
-
basePath = "/",
|
|
112
134
|
full = false,
|
|
113
135
|
includeDrafts = false,
|
|
114
136
|
getSummary,
|
|
137
|
+
groupByCategory = false,
|
|
115
138
|
} = options;
|
|
139
|
+
// `baseUrl` wins when both are provided — see the option JSDoc.
|
|
140
|
+
const basePath = options.baseUrl ?? options.basePath ?? "/";
|
|
116
141
|
|
|
117
142
|
const lines: string[] = [];
|
|
118
143
|
if (siteName) {
|
|
@@ -140,23 +165,54 @@ export async function generateLLMSTxt(
|
|
|
140
165
|
|
|
141
166
|
lines.push(`## ${input.name}`);
|
|
142
167
|
lines.push("");
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
|
|
169
|
+
if (groupByCategory) {
|
|
170
|
+
// Bucket entries by their first slug segment. Entries without
|
|
171
|
+
// a slash go under the "__root__" sentinel so we can render
|
|
172
|
+
// them above the categorized groups.
|
|
173
|
+
const groups = new Map<string, typeof sorted>();
|
|
174
|
+
for (const entry of sorted) {
|
|
175
|
+
const [head, ...rest] = entry.slug.split("/");
|
|
176
|
+
const key = rest.length > 0 ? head : "__root__";
|
|
177
|
+
const bucket = groups.get(key);
|
|
178
|
+
if (bucket) bucket.push(entry);
|
|
179
|
+
else groups.set(key, [entry]);
|
|
180
|
+
}
|
|
181
|
+
// Emit root-level entries first (if any), then categorized
|
|
182
|
+
// groups in alphabetical category order for determinism.
|
|
183
|
+
const rootEntries = groups.get("__root__") ?? [];
|
|
184
|
+
for (const entry of rootEntries) {
|
|
185
|
+
lines.push(renderEntryLine(entry, input.name, basePath, getSummary));
|
|
186
|
+
if (full) {
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push(entry.content);
|
|
189
|
+
lines.push("");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const categoryKeys = Array.from(groups.keys())
|
|
193
|
+
.filter((k) => k !== "__root__")
|
|
194
|
+
.sort();
|
|
195
|
+
for (const catKey of categoryKeys) {
|
|
196
|
+
if (rootEntries.length > 0) lines.push("");
|
|
197
|
+
lines.push(`### ${catKey}`);
|
|
159
198
|
lines.push("");
|
|
199
|
+
for (const entry of groups.get(catKey) ?? []) {
|
|
200
|
+
lines.push(renderEntryLine(entry, input.name, basePath, getSummary));
|
|
201
|
+
if (full) {
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push(entry.content);
|
|
204
|
+
lines.push("");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
for (const entry of sorted) {
|
|
210
|
+
lines.push(renderEntryLine(entry, input.name, basePath, getSummary));
|
|
211
|
+
if (full) {
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push(entry.content);
|
|
214
|
+
lines.push("");
|
|
215
|
+
}
|
|
160
216
|
}
|
|
161
217
|
}
|
|
162
218
|
lines.push("");
|
|
@@ -177,6 +233,31 @@ async function loadInput(input: LLMSTxtInput): Promise<CollectionEntry<unknown>[
|
|
|
177
233
|
return entries as CollectionEntry<unknown>[];
|
|
178
234
|
}
|
|
179
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Format a single entry as a `- [Title](href): summary` line. Split
|
|
238
|
+
* out from the main loop so both the flat and the categorized
|
|
239
|
+
* rendering paths share the same output shape.
|
|
240
|
+
*/
|
|
241
|
+
function renderEntryLine(
|
|
242
|
+
entry: CollectionEntry<unknown>,
|
|
243
|
+
collectionName: string,
|
|
244
|
+
basePath: string,
|
|
245
|
+
getSummary: ((entry: CollectionEntry<unknown>) => string) | undefined
|
|
246
|
+
): string {
|
|
247
|
+
const title =
|
|
248
|
+
typeof (entry.data as { title?: unknown })?.title === "string"
|
|
249
|
+
? String((entry.data as { title: string }).title)
|
|
250
|
+
: entry.slug || "index";
|
|
251
|
+
const href = joinHref(basePath, collectionName, entry.slug);
|
|
252
|
+
const summary = getSummary
|
|
253
|
+
? getSummary(entry)
|
|
254
|
+
: typeof (entry.data as { description?: unknown })?.description === "string"
|
|
255
|
+
? String((entry.data as { description: string }).description)
|
|
256
|
+
: "";
|
|
257
|
+
const tail = summary ? `: ${summary}` : "";
|
|
258
|
+
return `- [${title}](${href})${tail}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
180
261
|
function joinHref(base: string, collectionName: string, slug: string): string {
|
|
181
262
|
const parts = [collectionName, slug].filter((x) => x !== "" && x !== "/");
|
|
182
263
|
const tail = parts.join("/").replace(/\/+/g, "/");
|