blume 1.6.0 → 1.6.2
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/CHANGELOG.md +21 -0
- package/dist/cli/index.js +1318 -270
- package/dist/cli/index.js.map +68 -61
- package/dist/types/core/config-input.d.ts +9 -0
- package/dist/types/core/data.d.ts +12 -1
- package/dist/types/core/i18n-ui.d.ts +4 -0
- package/dist/types/core/schema.d.ts +7 -0
- package/dist/types/core/types.d.ts +6 -0
- package/dist/types/openapi/references.d.ts +5 -0
- package/docs/07-faq.mdx +9 -9
- package/docs/advanced/api-reference.mdx +10 -1
- package/docs/advanced/custom-pages.mdx +3 -1
- package/docs/advanced/graphql.mdx +1 -1
- package/docs/configuration/ai.mdx +76 -7
- package/docs/configuration/seo.mdx +3 -3
- package/docs/configuration/theming.mdx +6 -0
- package/docs/content/components.mdx +8 -1
- package/docs/index.mdx +2 -2
- package/package.json +53 -53
- package/skills/blume/SKILL.md +2 -2
- package/src/ai/agent-readability.ts +60 -17
- package/src/ai/api/handlers.ts +273 -0
- package/src/ai/api/paths.ts +14 -0
- package/src/ai/api/problem.ts +63 -0
- package/src/ai/api/spec.ts +681 -0
- package/src/ai/api-catalog.ts +11 -1
- package/src/ai/link-headers.ts +12 -3
- package/src/ai/llms.ts +9 -2
- package/src/ai/mcp/query.ts +390 -0
- package/src/ai/mcp/server.ts +32 -352
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +256 -64
- package/src/astro/index.ts +7 -0
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +398 -38
- package/src/cli/commands/build.ts +9 -1
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/PageActions.astro +25 -14
- package/src/core/config-input.ts +9 -0
- package/src/core/data.ts +17 -2
- package/src/core/define-components.ts +2 -0
- package/src/core/i18n-ui.ts +3 -0
- package/src/core/includes.ts +2 -1
- package/src/core/manifest.ts +10 -0
- package/src/core/schema.ts +20 -5
- package/src/core/types.ts +6 -0
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +1 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +5 -3
- package/src/deploy/vercel-negotiation.ts +97 -6
- package/src/og/card.ts +1 -1
- package/src/openapi/references.ts +8 -0
- package/src/openapi/render-mdx.ts +18 -4
- package/src/openapi/scalar.ts +0 -4
- package/src/registry/eject.ts +36 -17
- package/src/theme/entry.ts +2 -2
- package/src/theme/sources.ts +49 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { withBasePath } from "../../core/base-path.ts";
|
|
2
|
+
import { absoluteUrl, siteRoot } from "../../core/site-url.ts";
|
|
3
|
+
import {
|
|
4
|
+
API_NAVIGATION_PATH,
|
|
5
|
+
API_PAGE_PATH,
|
|
6
|
+
API_PAGES_PATH,
|
|
7
|
+
API_SEARCH_PATH,
|
|
8
|
+
} from "./paths.ts";
|
|
9
|
+
import { PROBLEM_TYPE } from "./problem.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The OpenAPI 3.1 description of a Blume site's machine-readable surface,
|
|
13
|
+
* served at `/openapi.json`. It documents the JSON docs API (`/api/docs/…`)
|
|
14
|
+
* in full — one `operationId` and description per operation, typed
|
|
15
|
+
* parameters, response schemas, and the RFC 9457 problem shape every error
|
|
16
|
+
* uses — and lists the text surfaces alongside (the `.md` mirrors, `llms.txt`,
|
|
17
|
+
* `llms-full.txt`, `agent-readability.json`) plus the MCP endpoint, so a
|
|
18
|
+
* function-calling framework that ingests OpenAPI gets the same reach an MCP
|
|
19
|
+
* client has. Generated per build from config, so it only describes what the
|
|
20
|
+
* deployed site actually serves.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** What the document needs to know about the site. */
|
|
24
|
+
export interface ApiSpecInput {
|
|
25
|
+
/** Whether `agent-readability.json` is published. */
|
|
26
|
+
agentReadability: boolean;
|
|
27
|
+
/** Normalized `deployment.base` (`""` or `/seg`). */
|
|
28
|
+
base: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
/** Whether `llms.txt`/`llms-full.txt` are published. */
|
|
31
|
+
llmsTxt: boolean;
|
|
32
|
+
/** The MCP server's route, or null when the server is off. */
|
|
33
|
+
mcpRoute: string | null;
|
|
34
|
+
name: string;
|
|
35
|
+
/** Whether the live search endpoint exists (server output only). */
|
|
36
|
+
search: boolean;
|
|
37
|
+
site: string | null;
|
|
38
|
+
/** The generating Blume version — the API contract's version. */
|
|
39
|
+
version: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A schema reference into `components.schemas`. */
|
|
43
|
+
interface SchemaRef {
|
|
44
|
+
$ref: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** An inline schema, as the text and generic-object responses use. */
|
|
48
|
+
interface InlineSchema {
|
|
49
|
+
type: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface MediaType {
|
|
53
|
+
schema: InlineSchema | SchemaRef;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A response object: its description and the media types it may carry. */
|
|
57
|
+
interface ResponseObject {
|
|
58
|
+
content?: Record<string, MediaType>;
|
|
59
|
+
description: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A parameter object, as this builder emits it. */
|
|
63
|
+
interface Parameter {
|
|
64
|
+
description: string;
|
|
65
|
+
explode?: boolean;
|
|
66
|
+
in: "path" | "query";
|
|
67
|
+
name: string;
|
|
68
|
+
required: boolean;
|
|
69
|
+
schema: object;
|
|
70
|
+
style?: "deepObject" | "form";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An operation object, as this builder emits it. */
|
|
74
|
+
export interface Operation {
|
|
75
|
+
description: string;
|
|
76
|
+
operationId: string;
|
|
77
|
+
parameters?: Parameter[];
|
|
78
|
+
requestBody?: { content: Record<string, MediaType>; required: boolean };
|
|
79
|
+
responses: Record<string, ResponseObject>;
|
|
80
|
+
summary: string;
|
|
81
|
+
tags: string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A path item: the HTTP methods this builder emits. */
|
|
85
|
+
export interface PathItem {
|
|
86
|
+
get?: Operation;
|
|
87
|
+
post?: Operation;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface Tag {
|
|
91
|
+
description: string;
|
|
92
|
+
name: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface Info {
|
|
96
|
+
description: string;
|
|
97
|
+
title: string;
|
|
98
|
+
version: string;
|
|
99
|
+
"x-generator": string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The OpenAPI document, as this builder emits it. */
|
|
103
|
+
export interface ApiSpecDocument {
|
|
104
|
+
components: { schemas: typeof schemas };
|
|
105
|
+
externalDocs?: { description: string; url: string };
|
|
106
|
+
info: Info;
|
|
107
|
+
openapi: "3.1.0";
|
|
108
|
+
paths: Record<string, PathItem>;
|
|
109
|
+
security: never[];
|
|
110
|
+
servers: { description: string; url: string }[];
|
|
111
|
+
tags: Tag[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const JSON_TYPE = "application/json";
|
|
115
|
+
const MARKDOWN_TYPE = "text/markdown";
|
|
116
|
+
const TEXT_TYPE = "text/plain";
|
|
117
|
+
|
|
118
|
+
const ref = (name: string): SchemaRef => ({
|
|
119
|
+
$ref: `#/components/schemas/${name}`,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const jsonResponse = (description: string, schema: string): ResponseObject => ({
|
|
123
|
+
content: { [JSON_TYPE]: { schema: ref(schema) } },
|
|
124
|
+
description,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const problemResponse = (description: string): ResponseObject => ({
|
|
128
|
+
content: { [PROBLEM_TYPE]: { schema: ref("Problem") } },
|
|
129
|
+
description,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const textResponse = (description: string, type: string): ResponseObject => ({
|
|
133
|
+
content: { [type]: { schema: { type: "string" } } },
|
|
134
|
+
description,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const string = (description: string) => ({ description, type: "string" });
|
|
138
|
+
|
|
139
|
+
const facetsSchema = {
|
|
140
|
+
additionalProperties: { type: "string" },
|
|
141
|
+
description:
|
|
142
|
+
"Facet values the site declares for the page's content type (`content.types.<type>.facets`), key → value.",
|
|
143
|
+
type: "object",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const versionProperty = {
|
|
147
|
+
description:
|
|
148
|
+
'Docs version the page belongs to on a versioned site: `""` for the current docs, else an archived version id. Absent on unversioned sites.',
|
|
149
|
+
type: "string",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** The shared filter parameters of the search operation. */
|
|
153
|
+
const searchParameters: Parameter[] = [
|
|
154
|
+
{
|
|
155
|
+
description: "The search query.",
|
|
156
|
+
in: "query",
|
|
157
|
+
name: "q",
|
|
158
|
+
required: true,
|
|
159
|
+
schema: { minLength: 1, type: "string" },
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
description: "Maximum hits to return (default 8, at most 20).",
|
|
163
|
+
in: "query",
|
|
164
|
+
name: "limit",
|
|
165
|
+
required: false,
|
|
166
|
+
schema: { default: 8, maximum: 20, minimum: 1, type: "integer" },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
description:
|
|
170
|
+
"Only include pages of these content types (frontmatter `type`, e.g. `doc`, `rfc`). Comma-separated or repeated. Omit for every type.",
|
|
171
|
+
explode: false,
|
|
172
|
+
in: "query",
|
|
173
|
+
name: "contentTypes",
|
|
174
|
+
required: false,
|
|
175
|
+
schema: { items: { type: "string" }, type: "array" },
|
|
176
|
+
style: "form",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
description:
|
|
180
|
+
"Only include pages in this locale (e.g. `fr`). Omit for every language.",
|
|
181
|
+
in: "query",
|
|
182
|
+
name: "locale",
|
|
183
|
+
required: false,
|
|
184
|
+
schema: { type: "string" },
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
description:
|
|
188
|
+
"Docs version to scope to on a versioned site: `latest` (the default — current docs only), `all`, or an archived version id. Ignored when the site is unversioned.",
|
|
189
|
+
in: "query",
|
|
190
|
+
name: "version",
|
|
191
|
+
required: false,
|
|
192
|
+
schema: { type: "string" },
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
description:
|
|
196
|
+
"Only include pages matching every facet, as `filters[key]=value` pairs (e.g. `filters[status]=enforced`). Facets are metadata the site declares per content type; the page index shows each page's values.",
|
|
197
|
+
explode: true,
|
|
198
|
+
in: "query",
|
|
199
|
+
name: "filters",
|
|
200
|
+
required: false,
|
|
201
|
+
schema: { additionalProperties: { type: "string" }, type: "object" },
|
|
202
|
+
style: "deepObject",
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const schemas = {
|
|
207
|
+
JsonRpcRequest: {
|
|
208
|
+
description: "A JSON-RPC 2.0 request, as the Model Context Protocol sends.",
|
|
209
|
+
properties: {
|
|
210
|
+
id: { description: "Request id (absent on notifications)." },
|
|
211
|
+
jsonrpc: { const: "2.0", type: "string" },
|
|
212
|
+
method: string("The MCP method, e.g. `initialize` or `tools/call`."),
|
|
213
|
+
params: { description: "Method parameters.", type: "object" },
|
|
214
|
+
},
|
|
215
|
+
required: ["jsonrpc", "method"],
|
|
216
|
+
type: "object",
|
|
217
|
+
},
|
|
218
|
+
JsonRpcResponse: {
|
|
219
|
+
description: "A JSON-RPC 2.0 response: a `result` or an `error`.",
|
|
220
|
+
properties: {
|
|
221
|
+
error: {
|
|
222
|
+
properties: {
|
|
223
|
+
code: { type: "integer" },
|
|
224
|
+
data: {},
|
|
225
|
+
message: { type: "string" },
|
|
226
|
+
},
|
|
227
|
+
required: ["code", "message"],
|
|
228
|
+
type: "object",
|
|
229
|
+
},
|
|
230
|
+
id: {},
|
|
231
|
+
jsonrpc: { const: "2.0", type: "string" },
|
|
232
|
+
result: { type: "object" },
|
|
233
|
+
},
|
|
234
|
+
required: ["jsonrpc"],
|
|
235
|
+
type: "object",
|
|
236
|
+
},
|
|
237
|
+
NavLink: {
|
|
238
|
+
properties: {
|
|
239
|
+
href: string("Link target — an internal route or an external URL."),
|
|
240
|
+
icon: string("Optional icon name."),
|
|
241
|
+
label: { type: "string" },
|
|
242
|
+
},
|
|
243
|
+
required: ["href", "label"],
|
|
244
|
+
type: "object",
|
|
245
|
+
},
|
|
246
|
+
NavNode: {
|
|
247
|
+
description:
|
|
248
|
+
"A sidebar entry: a page (linking to its route) or a group holding further nodes.",
|
|
249
|
+
oneOf: [
|
|
250
|
+
{
|
|
251
|
+
properties: {
|
|
252
|
+
badge: { type: "string" },
|
|
253
|
+
deprecated: { type: "boolean" },
|
|
254
|
+
description: { type: "string" },
|
|
255
|
+
icon: { type: "string" },
|
|
256
|
+
kind: { const: "page", type: "string" },
|
|
257
|
+
label: { type: "string" },
|
|
258
|
+
pageId: string("The page's stable content id."),
|
|
259
|
+
route: string("The page's route."),
|
|
260
|
+
},
|
|
261
|
+
required: ["kind", "label", "pageId", "route"],
|
|
262
|
+
type: "object",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
properties: {
|
|
266
|
+
badge: { type: "string" },
|
|
267
|
+
children: { items: ref("NavNode"), type: "array" },
|
|
268
|
+
collapsed: { type: "boolean" },
|
|
269
|
+
icon: { type: "string" },
|
|
270
|
+
kind: { const: "group", type: "string" },
|
|
271
|
+
label: { type: "string" },
|
|
272
|
+
path: string("The group's route prefix (not necessarily a page)."),
|
|
273
|
+
route: string("The group's index page route, when it has one."),
|
|
274
|
+
},
|
|
275
|
+
required: ["children", "kind", "label"],
|
|
276
|
+
type: "object",
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
NavSelector: {
|
|
281
|
+
description:
|
|
282
|
+
"A top-level partition selector (products, versions, languages).",
|
|
283
|
+
properties: {
|
|
284
|
+
items: {
|
|
285
|
+
items: {
|
|
286
|
+
properties: {
|
|
287
|
+
description: { type: "string" },
|
|
288
|
+
label: { type: "string" },
|
|
289
|
+
path: { type: "string" },
|
|
290
|
+
tag: { type: "string" },
|
|
291
|
+
},
|
|
292
|
+
required: ["label", "path"],
|
|
293
|
+
type: "object",
|
|
294
|
+
},
|
|
295
|
+
type: "array",
|
|
296
|
+
},
|
|
297
|
+
kind: { type: "string" },
|
|
298
|
+
label: { type: "string" },
|
|
299
|
+
},
|
|
300
|
+
required: ["items", "kind", "label"],
|
|
301
|
+
type: "object",
|
|
302
|
+
},
|
|
303
|
+
NavTab: {
|
|
304
|
+
properties: {
|
|
305
|
+
href: string(
|
|
306
|
+
"The clickable target when it differs from `path` (the section's first page)."
|
|
307
|
+
),
|
|
308
|
+
icon: { type: "string" },
|
|
309
|
+
label: { type: "string" },
|
|
310
|
+
path: string("The tab's section prefix."),
|
|
311
|
+
},
|
|
312
|
+
required: ["label", "path"],
|
|
313
|
+
type: "object",
|
|
314
|
+
},
|
|
315
|
+
Navigation: {
|
|
316
|
+
description:
|
|
317
|
+
"The docs navigation model: header tabs, the sidebar tree, partition selectors, and pinned links.",
|
|
318
|
+
properties: {
|
|
319
|
+
actions: { items: ref("NavLink"), type: "array" },
|
|
320
|
+
cta: { oneOf: [ref("NavLink"), { type: "null" }] },
|
|
321
|
+
featured: { items: ref("NavLink"), type: "array" },
|
|
322
|
+
repoUrl: { type: ["string", "null"] },
|
|
323
|
+
root: string("The tree root's route (`/`, or a locale/version prefix)."),
|
|
324
|
+
selectors: { items: ref("NavSelector"), type: "array" },
|
|
325
|
+
sidebar: { items: ref("NavNode"), type: "array" },
|
|
326
|
+
tabs: { items: ref("NavTab"), type: "array" },
|
|
327
|
+
},
|
|
328
|
+
required: ["featured", "selectors", "sidebar", "tabs"],
|
|
329
|
+
type: "object",
|
|
330
|
+
},
|
|
331
|
+
Page: {
|
|
332
|
+
allOf: [
|
|
333
|
+
ref("PageSummary"),
|
|
334
|
+
{
|
|
335
|
+
properties: {
|
|
336
|
+
markdown: string(
|
|
337
|
+
"The page as agent Markdown: frontmatter included, components downleveled to plain Markdown."
|
|
338
|
+
),
|
|
339
|
+
},
|
|
340
|
+
required: ["markdown"],
|
|
341
|
+
type: "object",
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
description: "A page's index entry plus its full Markdown body.",
|
|
345
|
+
},
|
|
346
|
+
PageSummary: {
|
|
347
|
+
properties: {
|
|
348
|
+
contentType: string("The page's content type (frontmatter `type`)."),
|
|
349
|
+
description: { type: "string" },
|
|
350
|
+
facets: facetsSchema,
|
|
351
|
+
json: {
|
|
352
|
+
description:
|
|
353
|
+
"This page's JSON representation (the `getPage` operation).",
|
|
354
|
+
format: "uri-reference",
|
|
355
|
+
type: "string",
|
|
356
|
+
},
|
|
357
|
+
lastModified: {
|
|
358
|
+
description: "ISO 8601 last-modified date, when known.",
|
|
359
|
+
type: ["string", "null"],
|
|
360
|
+
},
|
|
361
|
+
locale: string("The page's locale code."),
|
|
362
|
+
markdownUrl: {
|
|
363
|
+
description:
|
|
364
|
+
"The page's raw-Markdown mirror (the `getPageMarkdown` operation).",
|
|
365
|
+
format: "uri-reference",
|
|
366
|
+
type: "string",
|
|
367
|
+
},
|
|
368
|
+
route: string(
|
|
369
|
+
"The page's route (`/guides/install`); the key every other operation takes."
|
|
370
|
+
),
|
|
371
|
+
title: { type: "string" },
|
|
372
|
+
url: {
|
|
373
|
+
description: "Where the rendered page is served.",
|
|
374
|
+
format: "uri-reference",
|
|
375
|
+
type: "string",
|
|
376
|
+
},
|
|
377
|
+
version: versionProperty,
|
|
378
|
+
},
|
|
379
|
+
required: [
|
|
380
|
+
"contentType",
|
|
381
|
+
"json",
|
|
382
|
+
"lastModified",
|
|
383
|
+
"locale",
|
|
384
|
+
"markdownUrl",
|
|
385
|
+
"route",
|
|
386
|
+
"title",
|
|
387
|
+
"url",
|
|
388
|
+
],
|
|
389
|
+
type: "object",
|
|
390
|
+
},
|
|
391
|
+
PagesIndex: {
|
|
392
|
+
properties: {
|
|
393
|
+
count: { type: "integer" },
|
|
394
|
+
generator: string("The Blume version that built the site."),
|
|
395
|
+
pages: { items: ref("PageSummary"), type: "array" },
|
|
396
|
+
site: { type: ["string", "null"] },
|
|
397
|
+
},
|
|
398
|
+
required: ["count", "generator", "pages", "site"],
|
|
399
|
+
type: "object",
|
|
400
|
+
},
|
|
401
|
+
Problem: {
|
|
402
|
+
description:
|
|
403
|
+
"RFC 9457 problem details, with a stable code and a resolution hint.",
|
|
404
|
+
properties: {
|
|
405
|
+
code: string("Stable error code (e.g. `PAGE_NOT_FOUND`)."),
|
|
406
|
+
detail: string("Human-readable explanation of this occurrence."),
|
|
407
|
+
instance: string("The request path the problem occurred on."),
|
|
408
|
+
links: {
|
|
409
|
+
description: "Recovery links, when there is somewhere useful to go.",
|
|
410
|
+
items: {
|
|
411
|
+
properties: { href: { type: "string" }, label: { type: "string" } },
|
|
412
|
+
required: ["href", "label"],
|
|
413
|
+
type: "object",
|
|
414
|
+
},
|
|
415
|
+
type: "array",
|
|
416
|
+
},
|
|
417
|
+
resolution: string("What to do next."),
|
|
418
|
+
status: { type: "integer" },
|
|
419
|
+
title: { type: "string" },
|
|
420
|
+
type: string("Problem type URI; `about:blank` by default."),
|
|
421
|
+
},
|
|
422
|
+
required: ["code", "detail", "resolution", "status", "title", "type"],
|
|
423
|
+
type: "object",
|
|
424
|
+
},
|
|
425
|
+
SearchHit: {
|
|
426
|
+
properties: {
|
|
427
|
+
contentType: { type: "string" },
|
|
428
|
+
excerpt: string("The page description, else the start of its content."),
|
|
429
|
+
facets: facetsSchema,
|
|
430
|
+
route: string("The page's route; pass it to `getPage`."),
|
|
431
|
+
title: { type: "string" },
|
|
432
|
+
url: { format: "uri-reference", type: "string" },
|
|
433
|
+
version: versionProperty,
|
|
434
|
+
},
|
|
435
|
+
required: ["excerpt", "route", "title", "url"],
|
|
436
|
+
type: "object",
|
|
437
|
+
},
|
|
438
|
+
SearchResponse: {
|
|
439
|
+
properties: {
|
|
440
|
+
count: { type: "integer" },
|
|
441
|
+
query: { type: "string" },
|
|
442
|
+
results: { items: ref("SearchHit"), type: "array" },
|
|
443
|
+
},
|
|
444
|
+
required: ["count", "query", "results"],
|
|
445
|
+
type: "object",
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const ROUTE_PARAM: Parameter = {
|
|
450
|
+
description:
|
|
451
|
+
"The page route without its leading slash (`guides/install`), or `index` for the home page. May contain slashes.",
|
|
452
|
+
in: "path",
|
|
453
|
+
name: "route",
|
|
454
|
+
required: true,
|
|
455
|
+
schema: { type: "string" },
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
/** The document's `servers[0].url`: the site origin plus base, or the base. */
|
|
459
|
+
const serverUrl = (input: ApiSpecInput): string => {
|
|
460
|
+
if (input.site) {
|
|
461
|
+
return input.base
|
|
462
|
+
? absoluteUrl(input.site, input.base)
|
|
463
|
+
: siteRoot(input.site);
|
|
464
|
+
}
|
|
465
|
+
return input.base || "/";
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
/** Build the OpenAPI document. Plain data, ready to serialize. */
|
|
469
|
+
export const buildApiSpec = (input: ApiSpecInput): ApiSpecDocument => {
|
|
470
|
+
const pathEntries: [string, PathItem][] = [
|
|
471
|
+
[
|
|
472
|
+
API_PAGES_PATH,
|
|
473
|
+
{
|
|
474
|
+
get: {
|
|
475
|
+
description:
|
|
476
|
+
"Every documentation page with its route, title, description, content type, locale, facets, and the URLs of its rendered, Markdown, and JSON forms. Unfiltered; on a versioned site every version is listed with its `version`. Use it to enumerate the docs or to find a page when search is too narrow.",
|
|
477
|
+
operationId: "listPages",
|
|
478
|
+
responses: {
|
|
479
|
+
"200": jsonResponse("The page index.", "PagesIndex"),
|
|
480
|
+
default: problemResponse("An error, as problem details."),
|
|
481
|
+
},
|
|
482
|
+
summary: "List every page",
|
|
483
|
+
tags: ["Pages"],
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
[
|
|
488
|
+
API_PAGE_PATH,
|
|
489
|
+
{
|
|
490
|
+
get: {
|
|
491
|
+
description:
|
|
492
|
+
"A single page as JSON: its index entry plus the page's agent Markdown (frontmatter included, components downleveled to plain Markdown). Take `route` from `listPages` or `searchDocs`.",
|
|
493
|
+
operationId: "getPage",
|
|
494
|
+
parameters: [ROUTE_PARAM],
|
|
495
|
+
responses: {
|
|
496
|
+
"200": jsonResponse("The page.", "Page"),
|
|
497
|
+
"404": problemResponse("No page has that route."),
|
|
498
|
+
default: problemResponse("An error, as problem details."),
|
|
499
|
+
},
|
|
500
|
+
summary: "Get a page as JSON",
|
|
501
|
+
tags: ["Pages"],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
[
|
|
506
|
+
API_NAVIGATION_PATH,
|
|
507
|
+
{
|
|
508
|
+
get: {
|
|
509
|
+
description:
|
|
510
|
+
"The navigation tree (header tabs and the sidebar hierarchy) as readers see it, for the default locale and the current docs.",
|
|
511
|
+
operationId: "getNavigation",
|
|
512
|
+
responses: {
|
|
513
|
+
"200": jsonResponse("The navigation tree.", "Navigation"),
|
|
514
|
+
default: problemResponse("An error, as problem details."),
|
|
515
|
+
},
|
|
516
|
+
summary: "Get the navigation tree",
|
|
517
|
+
tags: ["Navigation"],
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
];
|
|
522
|
+
if (input.search) {
|
|
523
|
+
pathEntries.push([
|
|
524
|
+
API_SEARCH_PATH,
|
|
525
|
+
{
|
|
526
|
+
get: {
|
|
527
|
+
description:
|
|
528
|
+
"Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; narrow by content type, locale, version, or facet. Use it first to discover relevant pages, then `getPage` to read one in full.",
|
|
529
|
+
operationId: "searchDocs",
|
|
530
|
+
parameters: searchParameters,
|
|
531
|
+
responses: {
|
|
532
|
+
"200": jsonResponse(
|
|
533
|
+
"The matching pages, best first.",
|
|
534
|
+
"SearchResponse"
|
|
535
|
+
),
|
|
536
|
+
"400": problemResponse("The query was missing or blank."),
|
|
537
|
+
default: problemResponse("An error, as problem details."),
|
|
538
|
+
},
|
|
539
|
+
summary: "Search the docs",
|
|
540
|
+
tags: ["Search"],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
]);
|
|
544
|
+
}
|
|
545
|
+
pathEntries.push([
|
|
546
|
+
"/{route}.md",
|
|
547
|
+
{
|
|
548
|
+
get: {
|
|
549
|
+
description:
|
|
550
|
+
"A page's raw-Markdown mirror: append `.md` to any page URL. Components are downleveled to plain Markdown; `.mdx` serves the source as written. The same body the `getPage` operation carries in its `markdown` field.",
|
|
551
|
+
operationId: "getPageMarkdown",
|
|
552
|
+
parameters: [ROUTE_PARAM],
|
|
553
|
+
responses: {
|
|
554
|
+
"200": textResponse("The page as Markdown.", MARKDOWN_TYPE),
|
|
555
|
+
"404": textResponse(
|
|
556
|
+
"No page has that route; the body lists where to look next.",
|
|
557
|
+
MARKDOWN_TYPE
|
|
558
|
+
),
|
|
559
|
+
},
|
|
560
|
+
summary: "Get a page as Markdown",
|
|
561
|
+
tags: ["Markdown"],
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
]);
|
|
565
|
+
if (input.llmsTxt) {
|
|
566
|
+
pathEntries.push(
|
|
567
|
+
[
|
|
568
|
+
"/llms.txt",
|
|
569
|
+
{
|
|
570
|
+
get: {
|
|
571
|
+
description:
|
|
572
|
+
"The llms.txt index: the site's summary, when to use it, and every page with a one-line description, grouped by section.",
|
|
573
|
+
operationId: "getLlmsTxt",
|
|
574
|
+
responses: {
|
|
575
|
+
"200": textResponse("The index.", TEXT_TYPE),
|
|
576
|
+
},
|
|
577
|
+
summary: "Get llms.txt",
|
|
578
|
+
tags: ["Markdown"],
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
[
|
|
583
|
+
"/llms-full.txt",
|
|
584
|
+
{
|
|
585
|
+
get: {
|
|
586
|
+
description:
|
|
587
|
+
"The full Markdown of every current-docs page in one file.",
|
|
588
|
+
operationId: "getLlmsFullTxt",
|
|
589
|
+
responses: {
|
|
590
|
+
"200": textResponse("Every page's Markdown.", TEXT_TYPE),
|
|
591
|
+
},
|
|
592
|
+
summary: "Get llms-full.txt",
|
|
593
|
+
tags: ["Markdown"],
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
]
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
if (input.agentReadability) {
|
|
600
|
+
pathEntries.push([
|
|
601
|
+
"/agent-readability.json",
|
|
602
|
+
{
|
|
603
|
+
get: {
|
|
604
|
+
description:
|
|
605
|
+
"A manifest indexing every agent-facing artifact the site publishes — this API, the Markdown mirrors, llms.txt, the MCP server, feeds, and the sitemap.",
|
|
606
|
+
operationId: "getAgentReadability",
|
|
607
|
+
responses: {
|
|
608
|
+
"200": {
|
|
609
|
+
content: { [JSON_TYPE]: { schema: { type: "object" } } },
|
|
610
|
+
description: "The manifest.",
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
summary: "Get the agent-readability manifest",
|
|
614
|
+
tags: ["Discovery"],
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
]);
|
|
618
|
+
}
|
|
619
|
+
if (input.mcpRoute) {
|
|
620
|
+
pathEntries.push([
|
|
621
|
+
input.mcpRoute,
|
|
622
|
+
{
|
|
623
|
+
post: {
|
|
624
|
+
description:
|
|
625
|
+
"The Model Context Protocol server (Streamable HTTP, stateless, JSON responses). Tools: `search_docs`, `get_page`, `list_pages`, `get_navigation` — the same operations this API exposes — plus every page as a `text/markdown` resource. Discovery document at `/.well-known/mcp.json`.",
|
|
626
|
+
operationId: "mcp",
|
|
627
|
+
requestBody: {
|
|
628
|
+
content: { [JSON_TYPE]: { schema: ref("JsonRpcRequest") } },
|
|
629
|
+
required: true,
|
|
630
|
+
},
|
|
631
|
+
responses: {
|
|
632
|
+
"200": jsonResponse("The JSON-RPC response.", "JsonRpcResponse"),
|
|
633
|
+
},
|
|
634
|
+
summary: "Call the MCP server",
|
|
635
|
+
tags: ["MCP"],
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
]);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const tags: Tag[] = [
|
|
642
|
+
{ description: "Enumerate and read documentation pages.", name: "Pages" },
|
|
643
|
+
...(input.search
|
|
644
|
+
? [{ description: "Full-text search over the docs.", name: "Search" }]
|
|
645
|
+
: []),
|
|
646
|
+
{ description: "How the docs are organized.", name: "Navigation" },
|
|
647
|
+
{ description: "Plain-text and Markdown surfaces.", name: "Markdown" },
|
|
648
|
+
...(input.agentReadability
|
|
649
|
+
? [{ description: "Agent discovery documents.", name: "Discovery" }]
|
|
650
|
+
: []),
|
|
651
|
+
...(input.mcpRoute
|
|
652
|
+
? [{ description: "The Model Context Protocol endpoint.", name: "MCP" }]
|
|
653
|
+
: []),
|
|
654
|
+
];
|
|
655
|
+
|
|
656
|
+
const info: Info = {
|
|
657
|
+
description: [
|
|
658
|
+
`Read-only JSON API over the ${input.name} documentation${input.description ? `: ${input.description}` : "."}`,
|
|
659
|
+
"Every operation is public and needs no authentication. Errors are RFC 9457 problem details (`application/problem+json`) with a stable `code`, a `detail`, and a `resolution` hint.",
|
|
660
|
+
].join("\n\n"),
|
|
661
|
+
title: `${input.name} API`,
|
|
662
|
+
version: input.version,
|
|
663
|
+
"x-generator": `blume@${input.version}`,
|
|
664
|
+
};
|
|
665
|
+
const document: ApiSpecDocument = {
|
|
666
|
+
components: { schemas },
|
|
667
|
+
info,
|
|
668
|
+
openapi: "3.1.0",
|
|
669
|
+
paths: Object.fromEntries(pathEntries),
|
|
670
|
+
security: [],
|
|
671
|
+
servers: [{ description: input.name, url: serverUrl(input) }],
|
|
672
|
+
tags,
|
|
673
|
+
};
|
|
674
|
+
if (input.site) {
|
|
675
|
+
document.externalDocs = {
|
|
676
|
+
description: `${input.name} documentation`,
|
|
677
|
+
url: absoluteUrl(input.site, withBasePath(input.base, "/")),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return document;
|
|
681
|
+
};
|