@writedocs/generator 0.9.3 → 0.9.4
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 +1 -1
- package/src/components/ApiPlayground.astro +47 -6
- package/src/components/ApiSchemaField.astro +5 -2
- package/src/components/Hint.astro +46 -5
- package/src/components/Tooltip.astro +1 -0
- package/src/lib/llms-index.ts +1 -1
- package/src/lib/mcp-index.ts +4 -9
- package/src/lib/openapi-markdown.ts +154 -0
- package/src/pages/[...slug].astro +1 -1
- package/src/pages/[...slug].md.ts +3 -13
- package/src/pages/llms-full.txt.ts +3 -13
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type OpenApiOperation,
|
|
14
14
|
} from '../lib/openapi-render';
|
|
15
15
|
import { parseOpenApiRef } from '../lib/openapi-ref.js';
|
|
16
|
+
import { descriptionHtml } from '../lib/openapi-markdown';
|
|
16
17
|
|
|
17
18
|
interface Props {
|
|
18
19
|
operation: string; // "METHOD /path", matching a page's `openapi` frontmatter
|
|
@@ -37,6 +38,12 @@ const requestRows = requestSchema ? schemaRows(requestSchema) : [];
|
|
|
37
38
|
|
|
38
39
|
const responseEntries = op ? Object.entries(op.responses) : [];
|
|
39
40
|
const segments = op ? pathSegments(op.path) : [];
|
|
41
|
+
|
|
42
|
+
// Descriptions are Markdown in the spec - rendered here, once each
|
|
43
|
+
// (lib/openapi-markdown.ts), and set as HTML below.
|
|
44
|
+
const operationHtml = await descriptionHtml(op?.description);
|
|
45
|
+
const paramHtml = new Map(await Promise.all((op?.parameters ?? []).map(async (p) => [p, await descriptionHtml(p.description)] as const)));
|
|
46
|
+
const responseHtml = new Map(await Promise.all(responseEntries.map(async ([status, r]) => [status, await descriptionHtml(r.description)] as const)));
|
|
40
47
|
---
|
|
41
48
|
|
|
42
49
|
{!op && (
|
|
@@ -57,7 +64,7 @@ const segments = op ? pathSegments(op.path) : [];
|
|
|
57
64
|
)}
|
|
58
65
|
</code>
|
|
59
66
|
</div>
|
|
60
|
-
{
|
|
67
|
+
{operationHtml && <div class="wd-api-description wd-api-md" set:html={operationHtml} />}
|
|
61
68
|
|
|
62
69
|
{headerParams.length > 0 && (
|
|
63
70
|
<section class="wd-api-section">
|
|
@@ -70,7 +77,7 @@ const segments = op ? pathSegments(op.path) : [];
|
|
|
70
77
|
<span class="wd-api-pill wd-api-pill-type">{p.schema?.type ?? 'string'}</span>
|
|
71
78
|
{p.required && <span class="wd-api-pill wd-api-pill-required">required</span>}
|
|
72
79
|
</div>
|
|
73
|
-
{p
|
|
80
|
+
{paramHtml.get(p) && <div class="wd-api-param-desc wd-api-md" set:html={paramHtml.get(p)} />}
|
|
74
81
|
</div>
|
|
75
82
|
))}
|
|
76
83
|
</div>
|
|
@@ -88,7 +95,7 @@ const segments = op ? pathSegments(op.path) : [];
|
|
|
88
95
|
<span class="wd-api-pill wd-api-pill-type">{p.schema?.type ?? 'string'}</span>
|
|
89
96
|
{p.required && <span class="wd-api-pill wd-api-pill-required">required</span>}
|
|
90
97
|
</div>
|
|
91
|
-
{p
|
|
98
|
+
{paramHtml.get(p) && <div class="wd-api-param-desc wd-api-md" set:html={paramHtml.get(p)} />}
|
|
92
99
|
</div>
|
|
93
100
|
))}
|
|
94
101
|
</div>
|
|
@@ -106,7 +113,7 @@ const segments = op ? pathSegments(op.path) : [];
|
|
|
106
113
|
<span class="wd-api-pill wd-api-pill-type">{p.schema?.type ?? 'string'}</span>
|
|
107
114
|
{p.required && <span class="wd-api-pill wd-api-pill-required">required</span>}
|
|
108
115
|
</div>
|
|
109
|
-
{p
|
|
116
|
+
{paramHtml.get(p) && <div class="wd-api-param-desc wd-api-md" set:html={paramHtml.get(p)} />}
|
|
110
117
|
</div>
|
|
111
118
|
))}
|
|
112
119
|
</div>
|
|
@@ -132,8 +139,8 @@ const segments = op ? pathSegments(op.path) : [];
|
|
|
132
139
|
const rows = schema ? schemaRows(schema) : [];
|
|
133
140
|
return (
|
|
134
141
|
<Tab title={status}>
|
|
135
|
-
{
|
|
136
|
-
<
|
|
142
|
+
{responseHtml.get(status) && (
|
|
143
|
+
<div class="wd-api-param-desc-inline wd-api-response-desc wd-api-md" set:html={responseHtml.get(status)} />
|
|
137
144
|
)}
|
|
138
145
|
{rows.length > 0 && (
|
|
139
146
|
<div class="wd-api-param-list wd-api-param-list-nested">
|
|
@@ -282,4 +289,38 @@ const segments = op ? pathSegments(op.path) : [];
|
|
|
282
289
|
.wd-api-response-desc {
|
|
283
290
|
margin: 0 0 0.75rem;
|
|
284
291
|
}
|
|
292
|
+
/* A description from the spec, rendered from its Markdown
|
|
293
|
+
(lib/openapi-markdown.ts) - paragraphs, lists, code and tables sized
|
|
294
|
+
to sit under a field. A wide table scrolls sideways inside its own box
|
|
295
|
+
rather than widening the page. */
|
|
296
|
+
.wd-api-md > :first-child {
|
|
297
|
+
margin-top: 0;
|
|
298
|
+
}
|
|
299
|
+
.wd-api-md > :last-child {
|
|
300
|
+
margin-bottom: 0;
|
|
301
|
+
}
|
|
302
|
+
.wd-api-md p,
|
|
303
|
+
.wd-api-md ul,
|
|
304
|
+
.wd-api-md ol {
|
|
305
|
+
margin: 0 0 0.6em;
|
|
306
|
+
}
|
|
307
|
+
.wd-api-md ul,
|
|
308
|
+
.wd-api-md ol {
|
|
309
|
+
padding-left: 1.25rem;
|
|
310
|
+
}
|
|
311
|
+
.wd-api-md table {
|
|
312
|
+
display: block;
|
|
313
|
+
max-width: 100%;
|
|
314
|
+
overflow-x: auto;
|
|
315
|
+
border-collapse: collapse;
|
|
316
|
+
margin: 0.6rem 0;
|
|
317
|
+
font-size: 0.85rem;
|
|
318
|
+
}
|
|
319
|
+
.wd-api-md th,
|
|
320
|
+
.wd-api-md td {
|
|
321
|
+
padding: 0.4rem 0.75rem;
|
|
322
|
+
}
|
|
323
|
+
.wd-api-md a {
|
|
324
|
+
color: var(--wd-primary);
|
|
325
|
+
}
|
|
285
326
|
</style>
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// component handles every level."
|
|
21
21
|
import Expandable from './Expandable.astro';
|
|
22
22
|
import type { SchemaRow } from '../lib/openapi-render';
|
|
23
|
+
import { descriptionHtml } from '../lib/openapi-markdown';
|
|
23
24
|
|
|
24
25
|
interface Props {
|
|
25
26
|
rows: SchemaRow[];
|
|
@@ -33,17 +34,19 @@ interface Props {
|
|
|
33
34
|
showRequired?: boolean;
|
|
34
35
|
}
|
|
35
36
|
const { rows, showRequired = true } = Astro.props as Props;
|
|
37
|
+
// Descriptions are Markdown in the spec (lib/openapi-markdown.ts).
|
|
38
|
+
const html = await Promise.all(rows.map((row) => descriptionHtml(row.description)));
|
|
36
39
|
---
|
|
37
40
|
|
|
38
41
|
{
|
|
39
|
-
rows.map((row) => (
|
|
42
|
+
rows.map((row, i) => (
|
|
40
43
|
<div class="wd-api-param">
|
|
41
44
|
<div class="wd-api-param-head">
|
|
42
45
|
<span class="wd-api-param-name">{row.name}</span>
|
|
43
46
|
<span class="wd-api-pill wd-api-pill-type">{row.type}</span>
|
|
44
47
|
{showRequired && row.required && <span class="wd-api-pill wd-api-pill-required">required</span>}
|
|
45
48
|
</div>
|
|
46
|
-
{
|
|
49
|
+
{html[i] && <div class="wd-api-param-desc wd-api-md" set:html={html[i]} />}
|
|
47
50
|
{row.children.length > 0 && (
|
|
48
51
|
<Expandable title={`${row.name} properties`} defaultOpen={false}>
|
|
49
52
|
<Astro.self rows={row.children} showRequired={showRequired} />
|
|
@@ -4,8 +4,10 @@ import { extraClasses } from './class-names';
|
|
|
4
4
|
// (Callout, Card, Accordion, ...), which are all block-level and get
|
|
5
5
|
// their own line, a Hint sits mid-sentence: `word <Hint tip="...">this
|
|
6
6
|
// bit</Hint> continues`. That's why this renders a <span>, not a <div>,
|
|
7
|
-
// and why its own trigger area is
|
|
8
|
-
//
|
|
7
|
+
// and why its own trigger area is its slotted text plus, by default, a
|
|
8
|
+
// small info icon after it - a dotted underline and that icon are the
|
|
9
|
+
// whole cue, no padding box drawing attention to itself the way a Callout
|
|
10
|
+
// does. `icon={false}` leaves just the underlined text.
|
|
9
11
|
//
|
|
10
12
|
// tip is a plain string prop (not a slot) deliberately - the tooltip
|
|
11
13
|
// bubble itself needs to be one line of plain text CSS can center/
|
|
@@ -24,13 +26,20 @@ interface Props {
|
|
|
24
26
|
headline?: string;
|
|
25
27
|
cta?: string;
|
|
26
28
|
href?: string;
|
|
29
|
+
/** The small info icon after the text. On unless set to false. */
|
|
30
|
+
icon?: boolean;
|
|
27
31
|
}
|
|
28
|
-
const { tip, headline, cta, href } = Astro.props as Props;
|
|
32
|
+
const { tip, headline, cta, href, icon = true } = Astro.props as Props;
|
|
29
33
|
const hasLink = Boolean(cta && href);
|
|
30
34
|
---
|
|
31
35
|
|
|
32
36
|
<span class:list={["wd-hint", { "wd-hint-rich": Boolean(headline) || hasLink }, extraClasses(Astro.props)]} tabindex="0">
|
|
33
|
-
<span class="wd-hint-trigger"><slot /></span>
|
|
37
|
+
<span class="wd-hint-trigger"><slot /></span>{icon && (
|
|
38
|
+
<svg class="wd-hint-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
39
|
+
<circle cx="12" cy="12" r="9.5" />
|
|
40
|
+
<path d="M12 16.5v-5M12 8h.01" />
|
|
41
|
+
</svg>
|
|
42
|
+
)}
|
|
34
43
|
<span class="wd-hint-tooltip" role="tooltip">
|
|
35
44
|
{headline && <span class="wd-hint-headline">{headline}</span>}
|
|
36
45
|
<span class="wd-hint-tip">{tip}</span>
|
|
@@ -52,7 +61,39 @@ const hasLink = Boolean(cta && href);
|
|
|
52
61
|
hint here at all, since nothing about plain underlined text
|
|
53
62
|
otherwise signals "hover me" the way an icon or button would. */
|
|
54
63
|
cursor: help;
|
|
55
|
-
|
|
64
|
+
}
|
|
65
|
+
/* A dotted underline under the text, drawn by the font's own underline
|
|
66
|
+
rather than a border, so it follows the text across a line break and
|
|
67
|
+
sits clear of descenders. */
|
|
68
|
+
.wd-hint-trigger {
|
|
69
|
+
text-decoration-line: underline;
|
|
70
|
+
text-decoration-style: dotted;
|
|
71
|
+
text-decoration-thickness: 2px;
|
|
72
|
+
text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
|
|
73
|
+
text-underline-offset: 0.22em;
|
|
74
|
+
}
|
|
75
|
+
.wd-hint:hover .wd-hint-trigger,
|
|
76
|
+
.wd-hint:focus-visible .wd-hint-trigger {
|
|
77
|
+
text-decoration-color: currentColor;
|
|
78
|
+
}
|
|
79
|
+
/* Smaller than the text, a little clear of it, in the muted color. */
|
|
80
|
+
.wd-hint-icon {
|
|
81
|
+
display: inline-block;
|
|
82
|
+
width: 0.72em;
|
|
83
|
+
height: 0.72em;
|
|
84
|
+
margin-left: 0.22em;
|
|
85
|
+
vertical-align: -0.04em;
|
|
86
|
+
fill: none;
|
|
87
|
+
stroke: currentColor;
|
|
88
|
+
stroke-width: 2.2;
|
|
89
|
+
stroke-linecap: round;
|
|
90
|
+
stroke-linejoin: round;
|
|
91
|
+
color: var(--wd-text-muted);
|
|
92
|
+
transition: color 0.15s ease;
|
|
93
|
+
}
|
|
94
|
+
.wd-hint:hover .wd-hint-icon,
|
|
95
|
+
.wd-hint:focus-visible .wd-hint-icon {
|
|
96
|
+
color: var(--wd-text);
|
|
56
97
|
}
|
|
57
98
|
.wd-hint-tooltip {
|
|
58
99
|
position: absolute;
|
package/src/lib/llms-index.ts
CHANGED
|
@@ -62,7 +62,7 @@ export async function llmsIndexFiles(): Promise<Map<string, string> | null> {
|
|
|
62
62
|
// The .md route when it exists ([...slug].md.ts - unless `contextMenu` is off,
|
|
63
63
|
// and never for an OpenAPI page, which renders from the spec), else the
|
|
64
64
|
// HTML page.
|
|
65
|
-
const hasMarkdownRoute = contextMenuOptions(config.contextMenu).length > 0
|
|
65
|
+
const hasMarkdownRoute = contextMenuOptions(config.contextMenu).length > 0;
|
|
66
66
|
const href = (siteUrl ?? '') + (hasMarkdownRoute ? `/${slug}.md` : slug === 'index' ? '/' : `/${slug}/`);
|
|
67
67
|
let description = truncateDescription(entry.data.description);
|
|
68
68
|
// Mirrors Mintlify: an OpenAPI operation page's description gets its
|
package/src/lib/mcp-index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
resolveSections,
|
|
17
17
|
flattenNav,
|
|
18
18
|
} from './config';
|
|
19
|
-
import {
|
|
19
|
+
import { pageMarkdown } from './openapi-markdown';
|
|
20
20
|
import { orderByNavigation } from './llms.js';
|
|
21
21
|
import { writedocsTempDir } from './writedocs-temp-dir.js';
|
|
22
22
|
|
|
@@ -72,14 +72,9 @@ export async function mcpIndex(): Promise<McpIndex> {
|
|
|
72
72
|
.map((entry) => {
|
|
73
73
|
const slug = normalizeEntryId(entry.id);
|
|
74
74
|
const fileId = fileIdForEntry(contentDir, packageRoot, entry);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
variables: config.variables,
|
|
79
|
-
}).trim();
|
|
80
|
-
// An OpenAPI operation page renders from the spec, so its source body
|
|
81
|
-
// is near-empty - name the operation, at least.
|
|
82
|
-
if (entry.data.openapi) content = [`Operation: ${entry.data.openapi}`, content].filter(Boolean).join('\n\n');
|
|
75
|
+
// An API page's source is a near-empty stub - its operation, written
|
|
76
|
+
// out from the spec, follows its own text (lib/openapi-markdown.ts).
|
|
77
|
+
const content = pageMarkdown(entry, { contentDir, packageRoot, variables: config.variables });
|
|
83
78
|
return {
|
|
84
79
|
slug,
|
|
85
80
|
fileId,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// OpenAPI operations as Markdown, both ways:
|
|
2
|
+
//
|
|
3
|
+
// - descriptionHtml(): a spec's `description` fields are Markdown (CommonMark,
|
|
4
|
+
// and in practice GitHub tables too) - rendered to HTML for the API page,
|
|
5
|
+
// instead of printed as literal text with its `**`, backticks and pipes.
|
|
6
|
+
// Astro's own Markdown renderer, so a description reads like any page.
|
|
7
|
+
//
|
|
8
|
+
// - operationMarkdown() / pageMarkdown(): an operation page written out as
|
|
9
|
+
// Markdown - method and path, description, parameters, body, responses,
|
|
10
|
+
// examples. An API page's own source is a near-empty stub (it renders from
|
|
11
|
+
// the spec), so this is what its .md copy, llms-full.txt and the MCP
|
|
12
|
+
// index serve for it, and what "Copy page" copies.
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { createMarkdownProcessor } from '@astrojs/markdown-remark';
|
|
16
|
+
import {
|
|
17
|
+
schemaRows,
|
|
18
|
+
exampleFromSchema,
|
|
19
|
+
primaryContentType,
|
|
20
|
+
findOperationFile,
|
|
21
|
+
type OpenApiOperation,
|
|
22
|
+
type SchemaRow,
|
|
23
|
+
} from './openapi-render';
|
|
24
|
+
import { parseOpenApiRef } from './openapi-ref.js';
|
|
25
|
+
import { markdownForAgents } from './agent-markdown.js';
|
|
26
|
+
|
|
27
|
+
let renderer: ReturnType<typeof createMarkdownProcessor> | null = null;
|
|
28
|
+
const htmlCache = new Map<string, Promise<string>>();
|
|
29
|
+
|
|
30
|
+
/** A spec description as HTML. Empty for no description. */
|
|
31
|
+
export function descriptionHtml(markdown: string | null | undefined): Promise<string> {
|
|
32
|
+
const text = (markdown ?? '').trim();
|
|
33
|
+
if (!text) return Promise.resolve('');
|
|
34
|
+
let html = htmlCache.get(text);
|
|
35
|
+
if (!html) {
|
|
36
|
+
renderer ??= createMarkdownProcessor({ syntaxHighlight: false });
|
|
37
|
+
html = renderer.then((r) => r.render(text)).then((result) => result.code.trim());
|
|
38
|
+
htmlCache.set(text, html);
|
|
39
|
+
}
|
|
40
|
+
return html;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The operation a page's `openapi` frontmatter names, or null. */
|
|
44
|
+
export function loadOperation(ref: string, contentDir: string): OpenApiOperation | null {
|
|
45
|
+
const parsed = parseOpenApiRef(ref);
|
|
46
|
+
if (!parsed) return null;
|
|
47
|
+
const file = findOperationFile(contentDir, parsed.method, parsed.path, parsed.spec);
|
|
48
|
+
return file ? (JSON.parse(fs.readFileSync(file, 'utf-8')) as OpenApiOperation) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A description placed under a list item: every line indented to sit inside it. */
|
|
52
|
+
function indented(text: string, spaces: number): string {
|
|
53
|
+
const pad = ' '.repeat(spaces);
|
|
54
|
+
return text
|
|
55
|
+
.trim()
|
|
56
|
+
.split('\n')
|
|
57
|
+
.map((line) => (line.trim() ? pad + line : ''))
|
|
58
|
+
.join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fieldList(rows: SchemaRow[], depth = 0): string {
|
|
62
|
+
const pad = ' '.repeat(depth);
|
|
63
|
+
return rows
|
|
64
|
+
.map((row) => {
|
|
65
|
+
const head = `${pad}- \`${row.name}\` (${row.type}${row.required ? ', required' : ''})`;
|
|
66
|
+
const desc = row.description ? `\n${indented(row.description, pad.length + 2)}` : '';
|
|
67
|
+
const children = row.children.length ? `\n${fieldList(row.children, depth + 1)}` : '';
|
|
68
|
+
return head + desc + children;
|
|
69
|
+
})
|
|
70
|
+
.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function jsonBlock(value: unknown): string {
|
|
74
|
+
return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The operation, written out as Markdown (no title - the page has its own). */
|
|
78
|
+
export function operationMarkdown(op: OpenApiOperation): string {
|
|
79
|
+
const out: string[] = [];
|
|
80
|
+
out.push(`**${op.method.toUpperCase()}** \`${op.path}\``);
|
|
81
|
+
if (op.servers?.[0]?.url) out.push(`Base URL: \`${op.servers[0].url}\``);
|
|
82
|
+
if (op.description?.trim()) out.push(op.description.trim());
|
|
83
|
+
else if (op.summary?.trim()) out.push(op.summary.trim());
|
|
84
|
+
|
|
85
|
+
if (op.security?.length) {
|
|
86
|
+
const auth = op.security.map((s) => (s.scheme ? `${s.name} (${s.type}, ${s.scheme})` : s.in ? `${s.name} (${s.type} in ${s.in})` : `${s.name} (${s.type})`));
|
|
87
|
+
out.push(`## Authorization\n\n${auth.map((a) => `- ${a}`).join('\n')}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const [where, title] of [['header', 'Header parameters'], ['path', 'Path parameters'], ['query', 'Query parameters']] as const) {
|
|
91
|
+
const params = op.parameters.filter((p) => p.in === where);
|
|
92
|
+
if (!params.length) continue;
|
|
93
|
+
const list = params
|
|
94
|
+
.map((p) => {
|
|
95
|
+
const head = `- \`${p.name}\` (${p.schema?.type ?? 'string'}${p.required ? ', required' : ''})`;
|
|
96
|
+
return p.description ? `${head}\n${indented(p.description, 2)}` : head;
|
|
97
|
+
})
|
|
98
|
+
.join('\n');
|
|
99
|
+
out.push(`## ${title}\n\n${list}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const bodyType = primaryContentType(op.requestBody?.content);
|
|
103
|
+
const bodySchema = bodyType ? op.requestBody?.content?.[bodyType]?.schema : undefined;
|
|
104
|
+
if (bodyType && bodySchema) {
|
|
105
|
+
const rows = schemaRows(bodySchema);
|
|
106
|
+
const parts = [`## Body\n\nContent type: \`${bodyType}\``];
|
|
107
|
+
if (rows.length) parts.push(fieldList(rows));
|
|
108
|
+
const example = exampleFromSchema(bodySchema);
|
|
109
|
+
if (example !== null && example !== undefined) parts.push(`Example:\n\n${jsonBlock(example)}`);
|
|
110
|
+
out.push(parts.join('\n\n'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const responses = Object.entries(op.responses ?? {});
|
|
114
|
+
if (responses.length) {
|
|
115
|
+
const parts = ['## Responses'];
|
|
116
|
+
for (const [status, resp] of responses) {
|
|
117
|
+
parts.push(`### ${status}${resp.description?.trim() ? `\n\n${resp.description.trim()}` : ''}`);
|
|
118
|
+
const type = primaryContentType(resp.content);
|
|
119
|
+
const schema = type ? resp.content?.[type]?.schema : undefined;
|
|
120
|
+
if (schema) {
|
|
121
|
+
const rows = schemaRows(schema);
|
|
122
|
+
if (rows.length) parts.push(fieldList(rows));
|
|
123
|
+
const example = exampleFromSchema(schema);
|
|
124
|
+
if (example !== null && example !== undefined) parts.push(`Example:\n\n${jsonBlock(example)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
out.push(parts.join('\n\n'));
|
|
128
|
+
}
|
|
129
|
+
return out.join('\n\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface PageEntry {
|
|
133
|
+
body?: string;
|
|
134
|
+
filePath?: string;
|
|
135
|
+
data: { openapi?: string };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A page as agents read it (its .md copy, llms-full.txt, the MCP index),
|
|
139
|
+
* without the title: its own Markdown, and for an API page the operation
|
|
140
|
+
* after it - in the order the page shows them. */
|
|
141
|
+
export function pageMarkdown(
|
|
142
|
+
entry: PageEntry,
|
|
143
|
+
{ contentDir, packageRoot, variables }: { contentDir: string; packageRoot: string; variables: Record<string, string> }
|
|
144
|
+
): string {
|
|
145
|
+
const own = markdownForAgents(entry.body ?? '', {
|
|
146
|
+
file: entry.filePath ? path.resolve(packageRoot, entry.filePath) : undefined,
|
|
147
|
+
contentDir,
|
|
148
|
+
variables,
|
|
149
|
+
}).trim();
|
|
150
|
+
if (!entry.data.openapi) return own;
|
|
151
|
+
const op = loadOperation(entry.data.openapi, contentDir);
|
|
152
|
+
const operation = op ? operationMarkdown(op) : `Operation: ${entry.data.openapi}`;
|
|
153
|
+
return [own, operation].filter(Boolean).join('\n\n');
|
|
154
|
+
}
|
|
@@ -443,7 +443,7 @@ const isCanvasMode = pageMode === "custom" || pageMode === "blank";
|
|
|
443
443
|
// those are excluded from the .md route this menu links to in the first
|
|
444
444
|
// place, which this mirrors on the UI side.
|
|
445
445
|
const contextMenuItems = contextMenuOptions(config.contextMenu, { mcp: config.mcp });
|
|
446
|
-
const showCopyPageMenu = contextMenuItems.length > 0 && !isCanvasMode
|
|
446
|
+
const showCopyPageMenu = contextMenuItems.length > 0 && !isCanvasMode;
|
|
447
447
|
const siteUrl = resolveSiteUrl(config);
|
|
448
448
|
|
|
449
449
|
const components = {
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import type { APIRoute } from 'astro';
|
|
5
5
|
import { loadDocsConfig, normalizeEntryId, findAllPages, contextMenuOptions } from '../lib/config';
|
|
6
6
|
import { writedocsTempDir } from '../lib/writedocs-temp-dir.js';
|
|
7
|
-
import {
|
|
7
|
+
import { pageMarkdown } from '../lib/openapi-markdown';
|
|
8
8
|
|
|
9
9
|
// The raw-Markdown twin of [...slug].astro: every content page is also
|
|
10
10
|
// reachable at the exact same slug with a literal ".md" suffix (e.g.
|
|
@@ -46,13 +46,6 @@ export async function getStaticPaths() {
|
|
|
46
46
|
const entries: DocsEntry[] = [...pagesEntries, ...generatedDocsEntries];
|
|
47
47
|
|
|
48
48
|
return entries
|
|
49
|
-
// OpenAPI operation pages (generated stubs, or hand-written pages that
|
|
50
|
-
// set `openapi:` frontmatter) render almost entirely from the spec at
|
|
51
|
-
// request time (see ApiPlayground.astro) rather than from prose in
|
|
52
|
-
// entry.body - a raw .md dump of one would be near-empty and useless
|
|
53
|
-
// as an LLM-facing "view this page as text" export, so they're left
|
|
54
|
-
// out of this route entirely rather than serving something misleading.
|
|
55
|
-
.filter((entry) => !entry.data.openapi)
|
|
56
49
|
// A frontmatter `url` page's HTML route is a redirect to that link -
|
|
57
50
|
// there's no page content to serve as Markdown.
|
|
58
51
|
.filter((entry) => !entry.data.url)
|
|
@@ -81,11 +74,8 @@ export const GET: APIRoute = ({ props }) => {
|
|
|
81
74
|
// What the page shows rather than its raw source: Mintlify's <Visibility>
|
|
82
75
|
// for agents, imported snippets inlined, `variables` filled in - see
|
|
83
76
|
// lib/agent-markdown.js (llms-full.txt uses the same).
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
contentDir,
|
|
87
|
-
variables,
|
|
88
|
-
});
|
|
77
|
+
// An API page's operation is written out after its own text (lib/openapi-markdown.ts).
|
|
78
|
+
const body = pageMarkdown(entry, { contentDir, packageRoot, variables });
|
|
89
79
|
const markdown = `# ${entry.data.title}\n\n${body}`;
|
|
90
80
|
return new Response(markdown, {
|
|
91
81
|
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import type { APIRoute } from 'astro';
|
|
5
5
|
import { loadDocsConfig, normalizeEntryId, findAllPages, resolveSiteUrl, fileIdForEntry } from '../lib/config';
|
|
6
6
|
import { writedocsTempDir } from '../lib/writedocs-temp-dir.js';
|
|
7
|
-
import {
|
|
7
|
+
import { pageMarkdown } from '../lib/openapi-markdown';
|
|
8
8
|
import { orderByNavigation } from '../lib/llms.js';
|
|
9
9
|
|
|
10
10
|
// The "everything, concatenated" half of the llms.txt pair - see
|
|
@@ -43,13 +43,6 @@ export const GET: APIRoute = async () => {
|
|
|
43
43
|
const entries: DocsEntry[] = [...pagesEntries, ...generatedDocsEntries];
|
|
44
44
|
|
|
45
45
|
const unordered = entries
|
|
46
|
-
// Same exclusion [...slug].md.ts applies to its own per-page raw
|
|
47
|
-
// Markdown route, for the same reason: an OpenAPI operation page
|
|
48
|
-
// (generated stub, or a hand-written page that opts into rendering
|
|
49
|
-
// <ApiPlayground /> via `openapi:` frontmatter) renders almost
|
|
50
|
-
// entirely from the spec at request time, so entry.body alone is
|
|
51
|
-
// near-empty and not a meaningful "full content" contribution here.
|
|
52
|
-
.filter((entry) => !entry.data.openapi)
|
|
53
46
|
// Same noindex exclusion llms.txt.ts applies to its own listing -
|
|
54
47
|
// see that file's comment.
|
|
55
48
|
.filter((entry) => !entry.data.seo?.noindex)
|
|
@@ -60,11 +53,8 @@ export const GET: APIRoute = async () => {
|
|
|
60
53
|
// What the page shows, not its raw source: <Visibility> for agents,
|
|
61
54
|
// imported snippets inlined, `variables` filled in - same as the .md
|
|
62
55
|
// route (lib/agent-markdown.js).
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
contentDir,
|
|
66
|
-
variables: config.variables,
|
|
67
|
-
});
|
|
56
|
+
// An API page's operation is written out after its own text (lib/openapi-markdown.ts).
|
|
57
|
+
const body = pageMarkdown(entry, { contentDir, packageRoot, variables: config.variables });
|
|
68
58
|
// The page's own URL, so an agent can cite it.
|
|
69
59
|
const url = `${siteUrl ?? ''}${slug === 'index' ? '/' : `/${slug}/`}`;
|
|
70
60
|
return {
|