radiant-docs 0.1.59 → 0.1.61

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 (40) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +24 -2
  3. package/template/package-lock.json +121 -508
  4. package/template/package.json +3 -2
  5. package/template/scripts/generate-og-images.mjs +338 -6
  6. package/template/scripts/generate-og-metadata.mjs +29 -0
  7. package/template/src/components/Footer.astro +1 -1
  8. package/template/src/components/Header.astro +3 -10
  9. package/template/src/components/OpenApiPage.astro +181 -843
  10. package/template/src/components/SidebarGroup.astro +1 -1
  11. package/template/src/components/ThemeSwitcher.astro +5 -15
  12. package/template/src/components/chat/AssistantEmbedPanel.tsx +18 -7
  13. package/template/src/components/endpoint/PlaygroundBar.astro +54 -9
  14. package/template/src/components/endpoint/PlaygroundField.astro +1 -1
  15. package/template/src/components/endpoint/PlaygroundForm.astro +9 -5
  16. package/template/src/components/endpoint/RequestSnippets.astro +6 -1
  17. package/template/src/components/endpoint/ResponseFieldTree.astro +17 -13
  18. package/template/src/components/endpoint/ResponseFields.astro +4 -6
  19. package/template/src/components/endpoint/ResponseSnippets.astro +6 -1
  20. package/template/src/components/sidebar/SidebarEndpointLink.astro +9 -12
  21. package/template/src/components/sidebar/SidebarOpenApi.astro +3 -9
  22. package/template/src/components/ui/Field.astro +18 -15
  23. package/template/src/components/ui/Tag.astro +16 -2
  24. package/template/src/layouts/Layout.astro +6 -12
  25. package/template/src/lib/ai-artifacts.ts +792 -0
  26. package/template/src/lib/mdx/remark-resolve-internal-links.ts +22 -8
  27. package/template/src/lib/oas.ts +5 -1
  28. package/template/src/lib/openapi/operation-doc.ts +1150 -0
  29. package/template/src/lib/page-description.ts +20 -0
  30. package/template/src/lib/routes.ts +73 -18
  31. package/template/src/lib/utils.ts +11 -0
  32. package/template/src/pages/[...slug]/index.md.ts +35 -0
  33. package/template/src/pages/[...spec].json.ts +33 -0
  34. package/template/src/pages/[...spec].yaml.ts +33 -0
  35. package/template/src/pages/[...spec].yml.ts +33 -0
  36. package/template/src/pages/index.md.ts +17 -0
  37. package/template/src/pages/llms-full.txt.ts +11 -0
  38. package/template/src/pages/llms.txt.ts +11 -0
  39. package/template/src/styles/global.css +18 -15
  40. package/template/src/styles/vaul.css +0 -255
@@ -0,0 +1,792 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getCollection } from "astro:content";
4
+ import { prependBasePath } from "./base-path";
5
+ import {
6
+ getAllRoutes,
7
+ resolveMdxPageTitle,
8
+ type Route,
9
+ type MdxRoute,
10
+ type OpenApiRoute,
11
+ } from "./routes";
12
+ import {
13
+ getOpenApiOperationDoc,
14
+ OPENAPI_REQUEST_SECTION_LABELS,
15
+ type OpenApiField,
16
+ type OpenApiOperationDoc,
17
+ type OpenApiRequestSectionKey,
18
+ type OpenApiRequestSectionVariantData,
19
+ } from "./openapi/operation-doc";
20
+ import { resolvePageDescription } from "./page-description";
21
+ import { getConfig } from "./validation";
22
+
23
+ const DOCS_ROOT = path.join(process.cwd(), "src/content/docs");
24
+ const MAX_DESCRIPTION_LENGTH = 300;
25
+
26
+ const OPENAPI_SPEC_EXTENSIONS = new Set([".json", ".yaml", ".yml"]);
27
+
28
+ export const MARKDOWN_CONTENT_TYPE = "text/markdown; charset=utf-8";
29
+ export const PLAIN_TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
30
+
31
+ export type AiMarkdownPage = {
32
+ filePath: string;
33
+ routeSlug?: string;
34
+ title: string;
35
+ description?: string;
36
+ markdownPath: string;
37
+ canonicalPath: string;
38
+ markdownUrl: string;
39
+ canonicalUrl: string;
40
+ sourcePath?: string;
41
+ source: string;
42
+ };
43
+
44
+ export type OpenApiSpecArtifact = {
45
+ source: string;
46
+ label: string;
47
+ url: string;
48
+ extension: string;
49
+ isRemote: boolean;
50
+ routeParam?: string;
51
+ sourcePath?: string;
52
+ contentType?: string;
53
+ };
54
+
55
+ function normalizeDocsRootRelativePath(value: string): string {
56
+ return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
57
+ }
58
+
59
+ function isRemoteUrl(value: string): boolean {
60
+ try {
61
+ const url = new URL(value);
62
+ return url.protocol === "http:" || url.protocol === "https:";
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function toPublicPath(pathname: string): string {
69
+ const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
70
+ return prependBasePath(normalized);
71
+ }
72
+
73
+ function getConfiguredSiteOrigin(): string | undefined {
74
+ const rawSiteUrl = process.env.DOCS_SITE_URL?.trim();
75
+ if (!rawSiteUrl) return undefined;
76
+
77
+ try {
78
+ return new URL(rawSiteUrl).origin;
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ }
83
+
84
+ function buildPublicUrl(pathname: string): string {
85
+ const publicPath = toPublicPath(pathname);
86
+ const siteOrigin = getConfiguredSiteOrigin();
87
+
88
+ if (!siteOrigin) return publicPath;
89
+
90
+ return new URL(publicPath, siteOrigin).toString();
91
+ }
92
+
93
+ function getMarkdownPathForSlug(routeSlug: string): string {
94
+ const normalizedSlug = routeSlug.replace(/^\/+/, "").replace(/\/+$/, "");
95
+ return normalizedSlug ? `/${normalizedSlug}.md` : "/index.md";
96
+ }
97
+
98
+ function getCanonicalPathForSlug(routeSlug: string): string {
99
+ const normalizedSlug = routeSlug.replace(/^\/+/, "").replace(/\/+$/, "");
100
+ return normalizedSlug ? `/${normalizedSlug}` : "/";
101
+ }
102
+
103
+ function getEntryFilePath(entry: { id: string }): string {
104
+ return entry.id.replace(/\.(md|mdx)$/i, "");
105
+ }
106
+
107
+ async function findExistingMdxSourcePath(filePath: string): Promise<string> {
108
+ const normalizedFilePath = normalizeDocsRootRelativePath(filePath);
109
+ const candidates = [
110
+ path.join(DOCS_ROOT, `${normalizedFilePath}.mdx`),
111
+ path.join(DOCS_ROOT, `${normalizedFilePath}.md`),
112
+ ];
113
+
114
+ for (const candidate of candidates) {
115
+ try {
116
+ const stat = await fs.stat(candidate);
117
+ if (stat.isFile()) return candidate;
118
+ } catch {
119
+ // Try the next source extension.
120
+ }
121
+ }
122
+
123
+ throw new Error(`Could not find MDX source for docs page "${filePath}".`);
124
+ }
125
+
126
+ async function readMdxSource(filePath: string): Promise<{
127
+ sourcePath: string;
128
+ source: string;
129
+ }> {
130
+ const sourcePath = await findExistingMdxSourcePath(filePath);
131
+ const source = await fs.readFile(sourcePath, "utf8");
132
+ return { sourcePath, source };
133
+ }
134
+
135
+ async function getMdxEntryByFilePath(filePath: string) {
136
+ const docs = await getCollection("docs");
137
+ const normalizedFilePath = normalizeDocsRootRelativePath(filePath);
138
+
139
+ return docs.find((entry) => getEntryFilePath(entry) === normalizedFilePath);
140
+ }
141
+
142
+ async function createMarkdownPage(args: {
143
+ filePath: string;
144
+ routeSlug?: string;
145
+ title?: string;
146
+ markdownPath?: string;
147
+ canonicalPath?: string;
148
+ }): Promise<AiMarkdownPage> {
149
+ const entry = await getMdxEntryByFilePath(args.filePath);
150
+ if (!entry) {
151
+ throw new Error(`Could not find content collection entry for "${args.filePath}".`);
152
+ }
153
+
154
+ const { sourcePath, source } = await readMdxSource(args.filePath);
155
+ const routeSlug = args.routeSlug ?? "";
156
+ const markdownPath = args.markdownPath ?? getMarkdownPathForSlug(routeSlug);
157
+ const canonicalPath = args.canonicalPath ?? getCanonicalPathForSlug(routeSlug);
158
+ const title =
159
+ args.title ??
160
+ resolveMdxPageTitle({
161
+ entry,
162
+ filePath: args.filePath,
163
+ });
164
+ const config = await getConfig();
165
+
166
+ return {
167
+ filePath: normalizeDocsRootRelativePath(args.filePath),
168
+ routeSlug,
169
+ title,
170
+ description: resolvePageDescription({
171
+ pageTitle: title,
172
+ pageDescription: entry.data?.description,
173
+ docsTitle: config.title,
174
+ }),
175
+ markdownPath,
176
+ canonicalPath,
177
+ markdownUrl: buildPublicUrl(markdownPath),
178
+ canonicalUrl: buildPublicUrl(canonicalPath),
179
+ sourcePath,
180
+ source,
181
+ };
182
+ }
183
+
184
+ export async function getMarkdownRoutePages(): Promise<AiMarkdownPage[]> {
185
+ const routes = await getAllRoutes();
186
+
187
+ return Promise.all(routes.map((route) => createRouteMarkdownPage(route)));
188
+ }
189
+
190
+ export async function getLlmsListedMarkdownPages(): Promise<AiMarkdownPage[]> {
191
+ const config = await getConfig();
192
+ const routes = await getAllRoutes();
193
+ const visibleRoutes = routes.filter((route) => !route.hidden);
194
+ const homeRoute = config.home
195
+ ? routes.find(
196
+ (route): route is MdxRoute =>
197
+ route.type === "mdx" && route.filePath === config.home,
198
+ )
199
+ : visibleRoutes[0];
200
+ const skipRouteIdentity = homeRoute?.routeIdentity;
201
+ const pages: AiMarkdownPage[] = [];
202
+
203
+ if (config.home) {
204
+ pages.push(
205
+ await createMarkdownPage({
206
+ filePath: config.home,
207
+ routeSlug: "",
208
+ title: homeRoute?.title,
209
+ markdownPath: "/index.md",
210
+ canonicalPath: "/",
211
+ }),
212
+ );
213
+ } else if (homeRoute) {
214
+ pages.push(
215
+ await createRouteMarkdownPage(homeRoute, {
216
+ markdownPath: "/index.md",
217
+ canonicalPath: "/",
218
+ }),
219
+ );
220
+ }
221
+
222
+ for (const route of routes) {
223
+ if (route.routeIdentity === skipRouteIdentity) continue;
224
+ pages.push(await createRouteMarkdownPage(route));
225
+ }
226
+
227
+ return pages;
228
+ }
229
+
230
+ export async function getHomeMarkdownPage(): Promise<AiMarkdownPage | null> {
231
+ const config = await getConfig();
232
+ const routes = await getAllRoutes();
233
+ const visibleRoutes = routes.filter((route) => !route.hidden);
234
+
235
+ if (!config.home) {
236
+ const fallbackRoute = visibleRoutes[0] ?? routes[0];
237
+
238
+ if (!fallbackRoute) return null;
239
+
240
+ return createRouteMarkdownPage(fallbackRoute, {
241
+ markdownPath: "/index.md",
242
+ canonicalPath: "/",
243
+ });
244
+ }
245
+
246
+ const homeRoute = routes.find(
247
+ (route): route is MdxRoute =>
248
+ route.type === "mdx" && route.filePath === config.home,
249
+ );
250
+
251
+ return createMarkdownPage({
252
+ filePath: config.home,
253
+ routeSlug: "",
254
+ title: homeRoute?.title,
255
+ markdownPath: "/index.md",
256
+ canonicalPath: "/",
257
+ });
258
+ }
259
+
260
+ function truncateDescription(description: string): string {
261
+ const firstLine = description
262
+ .replace(/\r\n/g, "\n")
263
+ .split("\n")[0]
264
+ .replace(/\s+/g, " ")
265
+ .trim();
266
+
267
+ if (firstLine.length <= MAX_DESCRIPTION_LENGTH) return firstLine;
268
+
269
+ return `${firstLine.slice(0, MAX_DESCRIPTION_LENGTH - 3).trimEnd()}...`;
270
+ }
271
+
272
+ function formatDocsListItem(page: AiMarkdownPage): string {
273
+ const description = page.description
274
+ ? `: ${truncateDescription(page.description)}`
275
+ : "";
276
+
277
+ return `- [${page.title}](${page.markdownUrl})${description}`;
278
+ }
279
+
280
+ function normalizeLocalOpenApiSource(source: string): string {
281
+ return normalizeDocsRootRelativePath(source);
282
+ }
283
+
284
+ function getOpenApiSpecContentType(extension: string): string {
285
+ switch (extension) {
286
+ case ".json":
287
+ return "application/json; charset=utf-8";
288
+ case ".yaml":
289
+ case ".yml":
290
+ return "application/yaml; charset=utf-8";
291
+ default:
292
+ return "application/octet-stream";
293
+ }
294
+ }
295
+
296
+ function getOpenApiSpecLabel(source: string): string {
297
+ if (isRemoteUrl(source)) {
298
+ try {
299
+ const url = new URL(source);
300
+ return path.posix.basename(url.pathname).replace(/\.[^.]+$/, "") || source;
301
+ } catch {
302
+ return source;
303
+ }
304
+ }
305
+
306
+ const normalized = normalizeLocalOpenApiSource(source);
307
+ return normalized.replace(/\.[^.]+$/, "") || normalized;
308
+ }
309
+
310
+ function createOpenApiSpecArtifact(source: string): OpenApiSpecArtifact | null {
311
+ if (isRemoteUrl(source)) {
312
+ let extension = "";
313
+ try {
314
+ extension = path.posix.extname(new URL(source).pathname).toLowerCase();
315
+ } catch {
316
+ extension = "";
317
+ }
318
+
319
+ return {
320
+ source,
321
+ label: getOpenApiSpecLabel(source),
322
+ url: source,
323
+ extension,
324
+ isRemote: true,
325
+ };
326
+ }
327
+
328
+ const normalizedSource = normalizeLocalOpenApiSource(source);
329
+ const extension = path.posix.extname(normalizedSource).toLowerCase();
330
+ if (!OPENAPI_SPEC_EXTENSIONS.has(extension)) return null;
331
+
332
+ const sourcePath = path.join(DOCS_ROOT, normalizedSource);
333
+ const routeParam = normalizedSource.slice(0, -extension.length);
334
+
335
+ return {
336
+ source: normalizedSource,
337
+ label: getOpenApiSpecLabel(normalizedSource),
338
+ url: buildPublicUrl(`/${normalizedSource}`),
339
+ extension,
340
+ isRemote: false,
341
+ routeParam,
342
+ sourcePath,
343
+ contentType: getOpenApiSpecContentType(extension),
344
+ };
345
+ }
346
+
347
+ export async function getOpenApiSpecArtifacts(): Promise<OpenApiSpecArtifact[]> {
348
+ const routes = await getAllRoutes();
349
+ const sources = new Set(
350
+ routes
351
+ .filter((route): route is OpenApiRoute => route.type === "openapi")
352
+ .map((route) => route.filePath),
353
+ );
354
+ const artifacts: OpenApiSpecArtifact[] = [];
355
+
356
+ for (const source of sources) {
357
+ const artifact = createOpenApiSpecArtifact(source);
358
+ if (artifact) artifacts.push(artifact);
359
+ }
360
+
361
+ return artifacts.sort((a, b) => a.source.localeCompare(b.source));
362
+ }
363
+
364
+ export async function getOpenApiSpecArtifactsByExtension(
365
+ extension: string,
366
+ ): Promise<OpenApiSpecArtifact[]> {
367
+ const normalizedExtension = extension.toLowerCase();
368
+ const artifacts = await getOpenApiSpecArtifacts();
369
+ return artifacts.filter(
370
+ (artifact) =>
371
+ !artifact.isRemote && artifact.extension === normalizedExtension,
372
+ );
373
+ }
374
+
375
+ export async function readOpenApiSpecArtifact(
376
+ artifact: OpenApiSpecArtifact,
377
+ ): Promise<string> {
378
+ if (!artifact.sourcePath) {
379
+ throw new Error(`OpenAPI spec "${artifact.source}" is not a local file.`);
380
+ }
381
+
382
+ return fs.readFile(artifact.sourcePath, "utf8");
383
+ }
384
+
385
+ function escapeMarkdownTableCell(value: unknown): string {
386
+ return String(value ?? "")
387
+ .replace(/\r\n?/g, "\n")
388
+ .replace(/\n/g, "<br>")
389
+ .replace(/\|/g, "\\|")
390
+ .trim();
391
+ }
392
+
393
+ function formatInlineCode(value: unknown): string {
394
+ return `\`${String(value ?? "").replace(/`/g, "\\`")}\``;
395
+ }
396
+
397
+ function formatFieldValue(value: unknown): string {
398
+ if (value === undefined) return "";
399
+ if (typeof value === "string") return value;
400
+ return JSON.stringify(value);
401
+ }
402
+
403
+ function formatFieldDescription(field: OpenApiField): string {
404
+ const details: string[] = [];
405
+
406
+ if (field.description.trim()) details.push(field.description.trim());
407
+ if (field.enum && field.enum.length > 0) {
408
+ details.push(
409
+ `Allowed values: ${field.enum.map(formatFieldValue).join(", ")}`,
410
+ );
411
+ }
412
+ if (field.hasDefault) {
413
+ details.push(`Default: ${formatFieldValue(field.defaultValue)}`);
414
+ }
415
+ if (field.minLength !== undefined) {
416
+ details.push(`Min length: ${field.minLength}`);
417
+ }
418
+ if (field.maxLength !== undefined) {
419
+ details.push(`Max length: ${field.maxLength}`);
420
+ }
421
+ if (field.minimum !== undefined) {
422
+ details.push(`Minimum: ${field.minimum}`);
423
+ }
424
+ if (field.maximum !== undefined) {
425
+ details.push(`Maximum: ${field.maximum}`);
426
+ }
427
+ if (field.exclusiveMinimum !== undefined) {
428
+ details.push(`Exclusive minimum: ${field.exclusiveMinimum}`);
429
+ }
430
+ if (field.exclusiveMaximum !== undefined) {
431
+ details.push(`Exclusive maximum: ${field.exclusiveMaximum}`);
432
+ }
433
+ if (field.style) details.push(`Style: ${field.style}`);
434
+ if (field.explode !== undefined) details.push(`Explode: ${field.explode}`);
435
+
436
+ return details.join(" ");
437
+ }
438
+
439
+ function getNestedFieldName(parentName: string, childName: string): string {
440
+ if (!parentName) return childName;
441
+ if (childName.startsWith("[")) return `${parentName}${childName}`;
442
+ return `${parentName}.${childName}`;
443
+ }
444
+
445
+ function flattenFields(
446
+ fields: OpenApiField[],
447
+ parentName = "",
448
+ ): Array<{ name: string; field: OpenApiField }> {
449
+ const rows: Array<{ name: string; field: OpenApiField }> = [];
450
+
451
+ fields.forEach((field) => {
452
+ const name = getNestedFieldName(parentName, field.name);
453
+ rows.push({ name, field });
454
+
455
+ if (field.nested && field.nested.length > 0) {
456
+ rows.push(...flattenFields(field.nested, name));
457
+ }
458
+ });
459
+
460
+ return rows;
461
+ }
462
+
463
+ function appendFieldTable(lines: string[], fields: OpenApiField[]) {
464
+ const rows = flattenFields(fields);
465
+ if (rows.length === 0) return;
466
+
467
+ lines.push(
468
+ "",
469
+ "| Name | Required | Type | Description |",
470
+ "| --- | --- | --- | --- |",
471
+ ...rows.map(({ name, field }) => {
472
+ const cells = [
473
+ escapeMarkdownTableCell(formatInlineCode(name)),
474
+ field.required ? "Yes" : "No",
475
+ escapeMarkdownTableCell(formatInlineCode(field.type)),
476
+ escapeMarkdownTableCell(formatFieldDescription(field)),
477
+ ];
478
+
479
+ return `| ${cells.join(" | ")} |`;
480
+ }),
481
+ );
482
+ }
483
+
484
+ function appendVariantFieldTables(args: {
485
+ lines: string[];
486
+ variants?: OpenApiRequestSectionVariantData;
487
+ }) {
488
+ const { variants } = args;
489
+ if (!variants || variants.variants.length === 0) return;
490
+
491
+ args.lines.push(
492
+ "",
493
+ variants.variantType === "anyOf"
494
+ ? "One or more variants may apply."
495
+ : "One of these variants applies.",
496
+ );
497
+
498
+ variants.variants.forEach((variant) => {
499
+ args.lines.push("", `### ${variant.label}`);
500
+ appendFieldTable(args.lines, variant.fields);
501
+ });
502
+ }
503
+
504
+ function appendRequestSection(args: {
505
+ lines: string[];
506
+ title: string;
507
+ fields: OpenApiField[];
508
+ variants?: OpenApiRequestSectionVariantData;
509
+ }) {
510
+ const hasFields = args.fields.length > 0;
511
+ const hasVariants = Boolean(args.variants?.variants.length);
512
+
513
+ if (!hasFields && !hasVariants) return;
514
+
515
+ args.lines.push("", `## ${args.title}`);
516
+ appendFieldTable(args.lines, args.fields);
517
+ appendVariantFieldTables({
518
+ lines: args.lines,
519
+ variants: args.variants,
520
+ });
521
+ }
522
+
523
+ function appendOpenApiRequestMarkdown(
524
+ lines: string[],
525
+ operationDoc: OpenApiOperationDoc,
526
+ ) {
527
+ const parameterSections: Array<{
528
+ key: Exclude<OpenApiRequestSectionKey, "body">;
529
+ title: string;
530
+ }> = [
531
+ { key: "path", title: OPENAPI_REQUEST_SECTION_LABELS.path },
532
+ { key: "query", title: OPENAPI_REQUEST_SECTION_LABELS.query },
533
+ { key: "header", title: OPENAPI_REQUEST_SECTION_LABELS.header },
534
+ { key: "cookie", title: OPENAPI_REQUEST_SECTION_LABELS.cookie },
535
+ ];
536
+
537
+ parameterSections.forEach((section) => {
538
+ appendRequestSection({
539
+ lines,
540
+ title: section.title,
541
+ fields: operationDoc.requestFields[section.key],
542
+ variants: operationDoc.requestSectionVariants[section.key],
543
+ });
544
+ });
545
+
546
+ if (
547
+ operationDoc.bodyDescription ||
548
+ operationDoc.requestFields.body.length > 0 ||
549
+ operationDoc.requestSectionVariants.body?.variants.length
550
+ ) {
551
+ lines.push("", "## Request body");
552
+ if (operationDoc.bodyDescription) {
553
+ lines.push("", operationDoc.bodyDescription.trim());
554
+ }
555
+ appendFieldTable(lines, operationDoc.requestFields.body);
556
+ appendVariantFieldTables({
557
+ lines,
558
+ variants: operationDoc.requestSectionVariants.body,
559
+ });
560
+ }
561
+ }
562
+
563
+ function getResponseContentTypes(response: any): string[] {
564
+ const content = response?.content;
565
+ if (!content || typeof content !== "object") return [];
566
+ return Object.keys(content);
567
+ }
568
+
569
+ function appendOpenApiResponsesMarkdown(
570
+ lines: string[],
571
+ responses: unknown,
572
+ ) {
573
+ if (!responses || typeof responses !== "object") return;
574
+
575
+ const responseEntries = Object.entries(responses).filter(
576
+ ([status]) => !status.startsWith("x-"),
577
+ );
578
+ if (responseEntries.length === 0) return;
579
+
580
+ lines.push("", "## Responses");
581
+
582
+ responseEntries.forEach(([status, response]) => {
583
+ lines.push("", `### ${status}`);
584
+
585
+ if (
586
+ response &&
587
+ typeof response === "object" &&
588
+ typeof (response as any).description === "string" &&
589
+ (response as any).description.trim()
590
+ ) {
591
+ lines.push("", (response as any).description.trim());
592
+ }
593
+
594
+ const contentTypes = getResponseContentTypes(response);
595
+ if (contentTypes.length > 0) {
596
+ lines.push(
597
+ "",
598
+ `Content types: ${contentTypes.map(formatInlineCode).join(", ")}`,
599
+ );
600
+ }
601
+ });
602
+ }
603
+
604
+ function buildOpenApiMarkdown(args: {
605
+ route: OpenApiRoute;
606
+ operationDoc: OpenApiOperationDoc;
607
+ specUrl: string;
608
+ }): string {
609
+ const summary = args.operationDoc.description?.trim();
610
+ const lines = [
611
+ "---",
612
+ `title: ${JSON.stringify(args.route.title)}`,
613
+ `openapi: ${JSON.stringify(args.route.filePath)}`,
614
+ `method: ${JSON.stringify(args.route.openApiMethod.toUpperCase())}`,
615
+ `path: ${JSON.stringify(args.route.openApiPath)}`,
616
+ "---",
617
+ "",
618
+ `# ${args.route.title}`,
619
+ "",
620
+ `${args.route.openApiMethod.toUpperCase()} \`${args.route.openApiPath}\``,
621
+ ];
622
+
623
+ if (summary) {
624
+ lines.push("", summary);
625
+ }
626
+
627
+ if (args.specUrl) {
628
+ lines.push("", `OpenAPI spec: ${args.specUrl}`);
629
+ }
630
+
631
+ appendOpenApiRequestMarkdown(lines, args.operationDoc);
632
+ appendOpenApiResponsesMarkdown(lines, args.operationDoc.responses);
633
+
634
+ lines.push(
635
+ "",
636
+ "## OpenAPI operation",
637
+ "",
638
+ "```json",
639
+ JSON.stringify(args.operationDoc.operationSpecSlice, null, 2),
640
+ "```",
641
+ );
642
+
643
+ return `${lines.join("\n").trimEnd()}\n`;
644
+ }
645
+
646
+ async function createOpenApiMarkdownPage(args: {
647
+ route: OpenApiRoute;
648
+ markdownPath?: string;
649
+ canonicalPath?: string;
650
+ }): Promise<AiMarkdownPage> {
651
+ const artifact = createOpenApiSpecArtifact(args.route.filePath);
652
+ const operationDoc = await getOpenApiOperationDoc(args.route);
653
+ const config = await getConfig();
654
+ const description = resolvePageDescription({
655
+ pageTitle: args.route.title,
656
+ pageDescription: operationDoc.description,
657
+ docsTitle: config.title,
658
+ });
659
+ const routeSlug = args.route.slug;
660
+ const markdownPath = args.markdownPath ?? getMarkdownPathForSlug(routeSlug);
661
+ const canonicalPath =
662
+ args.canonicalPath ?? getCanonicalPathForSlug(routeSlug);
663
+
664
+ return {
665
+ filePath: normalizeDocsRootRelativePath(args.route.filePath),
666
+ routeSlug,
667
+ title: args.route.title,
668
+ description,
669
+ markdownPath,
670
+ canonicalPath,
671
+ markdownUrl: buildPublicUrl(markdownPath),
672
+ canonicalUrl: buildPublicUrl(canonicalPath),
673
+ sourcePath: artifact?.sourcePath,
674
+ source: buildOpenApiMarkdown({
675
+ route: args.route,
676
+ operationDoc,
677
+ specUrl: artifact?.url ?? "",
678
+ }),
679
+ };
680
+ }
681
+
682
+ async function createRouteMarkdownPage(
683
+ route: Route,
684
+ overrides: {
685
+ markdownPath?: string;
686
+ canonicalPath?: string;
687
+ } = {},
688
+ ): Promise<AiMarkdownPage> {
689
+ if (route.type === "mdx") {
690
+ return createMarkdownPage({
691
+ filePath: route.filePath,
692
+ routeSlug: route.slug,
693
+ title: route.title,
694
+ ...overrides,
695
+ });
696
+ }
697
+
698
+ return createOpenApiMarkdownPage({
699
+ route,
700
+ ...overrides,
701
+ });
702
+ }
703
+
704
+ function formatOpenApiSpecsSection(specs: OpenApiSpecArtifact[]): string[] {
705
+ if (specs.length === 0) return [];
706
+
707
+ return [
708
+ "## OpenAPI Specs",
709
+ "",
710
+ ...specs.map((spec) => `- [${spec.label}](${spec.url})`),
711
+ ];
712
+ }
713
+
714
+ async function readCustomArtifact(filename: string): Promise<string | null> {
715
+ const artifactPath = path.join(DOCS_ROOT, filename);
716
+
717
+ try {
718
+ const stat = await fs.stat(artifactPath);
719
+ if (!stat.isFile()) return null;
720
+ return fs.readFile(artifactPath, "utf8");
721
+ } catch {
722
+ return null;
723
+ }
724
+ }
725
+
726
+ export async function getLlmsTxt(): Promise<string> {
727
+ const custom = await readCustomArtifact("llms.txt");
728
+ if (custom !== null) return custom;
729
+
730
+ const config = await getConfig();
731
+ const pages = await getLlmsListedMarkdownPages();
732
+ const specs = await getOpenApiSpecArtifacts();
733
+ const lines = [`# ${config.title}`, "", "## Docs", ""];
734
+
735
+ lines.push(...pages.map(formatDocsListItem));
736
+
737
+ const specsSection = formatOpenApiSpecsSection(specs);
738
+ if (specsSection.length > 0) {
739
+ lines.push("", ...specsSection);
740
+ }
741
+
742
+ return `${lines.join("\n").trimEnd()}\n`;
743
+ }
744
+
745
+ function formatLlmsFullPage(page: AiMarkdownPage): string {
746
+ return [
747
+ `# ${page.title}`,
748
+ "",
749
+ `Source: ${page.markdownUrl}`,
750
+ `Canonical: ${page.canonicalUrl}`,
751
+ "",
752
+ page.source.trimEnd(),
753
+ ].join("\n");
754
+ }
755
+
756
+ function formatLlmsFullSpec(spec: OpenApiSpecArtifact, source: string): string {
757
+ const fenceLanguage = spec.extension === ".json" ? "json" : "yaml";
758
+ return [
759
+ `# ${spec.label}`,
760
+ "",
761
+ `Source: ${spec.url}`,
762
+ "",
763
+ `\`\`\`${fenceLanguage}`,
764
+ source.trimEnd(),
765
+ "```",
766
+ ].join("\n");
767
+ }
768
+
769
+ export async function getLlmsFullTxt(): Promise<string> {
770
+ const custom = await readCustomArtifact("llms-full.txt");
771
+ if (custom !== null) return custom;
772
+
773
+ const config = await getConfig();
774
+ const pages = await getLlmsListedMarkdownPages();
775
+ const specs = await getOpenApiSpecArtifacts();
776
+ const sections = [`# ${config.title}`];
777
+
778
+ for (const page of pages) {
779
+ sections.push(formatLlmsFullPage(page));
780
+ }
781
+
782
+ for (const spec of specs) {
783
+ if (spec.isRemote) {
784
+ sections.push([`# ${spec.label}`, "", `Source: ${spec.url}`].join("\n"));
785
+ continue;
786
+ }
787
+
788
+ sections.push(formatLlmsFullSpec(spec, await readOpenApiSpecArtifact(spec)));
789
+ }
790
+
791
+ return `${sections.join("\n\n---\n\n").trimEnd()}\n`;
792
+ }