blume 1.4.3 → 1.5.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/CHANGELOG.md +17 -0
- package/dist/cli/index.js +1621 -576
- package/dist/cli/index.js.map +109 -104
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +79 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +54 -29
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +152 -33
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/RootLayout.astro +18 -0
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +88 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +15 -3
- package/src/core/schema.ts +213 -67
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +30 -11
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
|
@@ -47,10 +47,31 @@ interface Splice {
|
|
|
47
47
|
text: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A statically-recovered data value. Parsed front matter and evaluated
|
|
52
|
+
* attribute literals are both plain data — scalars, dates, arrays, and
|
|
53
|
+
* nested maps — never functions or class instances.
|
|
54
|
+
*/
|
|
55
|
+
export type EvaluatedValue =
|
|
56
|
+
| string
|
|
57
|
+
| number
|
|
58
|
+
| boolean
|
|
59
|
+
| null
|
|
60
|
+
| undefined
|
|
61
|
+
| Date
|
|
62
|
+
| EvaluatedValue[]
|
|
63
|
+
| { [key: string]: EvaluatedValue };
|
|
64
|
+
|
|
65
|
+
const isString = <Value>(value: Value): value is Value & string =>
|
|
66
|
+
typeof value === "string";
|
|
67
|
+
|
|
68
|
+
const isNumber = <Value>(value: Value): value is Value & number =>
|
|
69
|
+
typeof value === "number";
|
|
70
|
+
|
|
50
71
|
/** Evaluated props plus whether any attribute resisted static evaluation. */
|
|
51
72
|
interface EvaluatedProps {
|
|
52
73
|
lossy: boolean;
|
|
53
|
-
props: Record<string,
|
|
74
|
+
props: Record<string, EvaluatedValue>;
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
/** A child component extracted by name (e.g. each `<Step>` under `<Steps>`). */
|
|
@@ -70,7 +91,7 @@ export interface ComponentMarkdownContext extends EvaluatedProps {
|
|
|
70
91
|
* serializer read page metadata directly, even when a prop expression is
|
|
71
92
|
* not statically evaluable.
|
|
72
93
|
*/
|
|
73
|
-
frontmatter: Record<string,
|
|
94
|
+
frontmatter: Record<string, EvaluatedValue>;
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
/**
|
|
@@ -93,15 +114,16 @@ export type ComponentMarkdown = (
|
|
|
93
114
|
*/
|
|
94
115
|
const evaluateExpression = (
|
|
95
116
|
raw: string,
|
|
96
|
-
frontmatter: Record<string,
|
|
97
|
-
)
|
|
117
|
+
frontmatter: Record<string, EvaluatedValue> | undefined
|
|
118
|
+
) => {
|
|
98
119
|
try {
|
|
99
120
|
// Build-time eval of the author's own attribute literals; a throw falls
|
|
100
121
|
// back to leaving the JSX verbatim.
|
|
101
122
|
// oxlint-disable-next-line no-new-func
|
|
102
|
-
const value = new Function(
|
|
103
|
-
frontmatter
|
|
104
|
-
|
|
123
|
+
const value: EvaluatedValue = new Function(
|
|
124
|
+
"frontmatter",
|
|
125
|
+
`"use strict"; return (${raw});`
|
|
126
|
+
)(frontmatter);
|
|
105
127
|
return { ok: true, value };
|
|
106
128
|
} catch {
|
|
107
129
|
return { ok: false, value: undefined };
|
|
@@ -111,9 +133,9 @@ const evaluateExpression = (
|
|
|
111
133
|
/** Evaluate an element's attributes into a plain props object. */
|
|
112
134
|
const readProps = (
|
|
113
135
|
node: MdastNode,
|
|
114
|
-
frontmatter: Record<string,
|
|
136
|
+
frontmatter: Record<string, EvaluatedValue> | undefined
|
|
115
137
|
): EvaluatedProps => {
|
|
116
|
-
const props: Record<string,
|
|
138
|
+
const props: Record<string, EvaluatedValue> = {};
|
|
117
139
|
let lossy = false;
|
|
118
140
|
for (const attribute of node.attributes ?? []) {
|
|
119
141
|
// A spread ({...props}) can't be recovered statically.
|
|
@@ -124,7 +146,7 @@ const readProps = (
|
|
|
124
146
|
if (attribute.value === null || attribute.value === undefined) {
|
|
125
147
|
// Boolean shorthand: `<Steps compact>`.
|
|
126
148
|
props[attribute.name] = true;
|
|
127
|
-
} else if (
|
|
149
|
+
} else if (isString(attribute.value)) {
|
|
128
150
|
props[attribute.name] = attribute.value;
|
|
129
151
|
} else {
|
|
130
152
|
const result = evaluateExpression(attribute.value.value, frontmatter);
|
|
@@ -198,35 +220,49 @@ const isJsxElement = (node: MdastNode): boolean =>
|
|
|
198
220
|
node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement";
|
|
199
221
|
|
|
200
222
|
/** Flatten a value to a single Markdown table cell (pipes escaped). */
|
|
201
|
-
const cellText = (value:
|
|
223
|
+
const cellText = (value: EvaluatedValue): string =>
|
|
202
224
|
String(value ?? "")
|
|
203
225
|
.replaceAll(/\s*\n\s*/gu, " ")
|
|
204
226
|
.replaceAll("|", "\\|")
|
|
205
227
|
.trim();
|
|
206
228
|
|
|
207
229
|
/** A cell rendered as inline code, unless the value itself uses backticks. */
|
|
208
|
-
const cellCode = (value:
|
|
230
|
+
const cellCode = (value: EvaluatedValue): string => {
|
|
209
231
|
const text = cellText(value);
|
|
210
232
|
return text && !text.includes("`") ? `\`${text}\`` : text;
|
|
211
233
|
};
|
|
212
234
|
|
|
213
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* One `<TypeTable type={{...}}>` entry, matching the component's props. The
|
|
237
|
+
* index signature keeps the interface interchangeable with the evaluated
|
|
238
|
+
* data-value maps it is narrowed from.
|
|
239
|
+
*/
|
|
214
240
|
interface TypeEntry {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
241
|
+
[field: string]: EvaluatedValue;
|
|
242
|
+
default?: EvaluatedValue;
|
|
243
|
+
description?: EvaluatedValue;
|
|
244
|
+
required?: EvaluatedValue;
|
|
245
|
+
type?: EvaluatedValue;
|
|
246
|
+
typeDescription?: EvaluatedValue;
|
|
247
|
+
typeDescriptionLink?: EvaluatedValue;
|
|
221
248
|
}
|
|
222
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The `type` data prop's entry map. Structural only: each entry's fields are
|
|
252
|
+
* rendered through cellText/cellCode, which stringify any value.
|
|
253
|
+
*/
|
|
254
|
+
const isTypeEntryMap = (
|
|
255
|
+
value: EvaluatedValue
|
|
256
|
+
): value is Record<string, TypeEntry> =>
|
|
257
|
+
typeof value === "object" && value !== null;
|
|
258
|
+
|
|
223
259
|
const typeTable: ComponentMarkdown = ({ children, props }) => {
|
|
224
260
|
const { type } = props;
|
|
225
|
-
if (type
|
|
261
|
+
if (!isTypeEntryMap(type)) {
|
|
226
262
|
// The data prop is missing or wasn't statically evaluable.
|
|
227
263
|
return null;
|
|
228
264
|
}
|
|
229
|
-
const entries = Object.entries(type
|
|
265
|
+
const entries = Object.entries(type);
|
|
230
266
|
const rows = entries.map(([name, info]) => {
|
|
231
267
|
const prop = cellCode(`${name}${info.required ? "" : "?"}`);
|
|
232
268
|
const typeCell = info.typeDescriptionLink
|
|
@@ -236,7 +272,7 @@ const typeTable: ComponentMarkdown = ({ children, props }) => {
|
|
|
236
272
|
info.default === undefined ? "-" : cellCode(info.default);
|
|
237
273
|
const description = cellText(
|
|
238
274
|
[info.description, info.typeDescription]
|
|
239
|
-
.filter((part) =>
|
|
275
|
+
.filter((part) => isString(part) && part !== "")
|
|
240
276
|
.join(" ")
|
|
241
277
|
);
|
|
242
278
|
return [prop, typeCell, defaultCell, description];
|
|
@@ -254,9 +290,9 @@ const typeTable: ComponentMarkdown = ({ children, props }) => {
|
|
|
254
290
|
};
|
|
255
291
|
|
|
256
292
|
const callout: ComponentMarkdown = ({ children, props }) => {
|
|
257
|
-
const type =
|
|
293
|
+
const type = isString(props.type) ? props.type : "info";
|
|
258
294
|
const label =
|
|
259
|
-
|
|
295
|
+
isString(props.title) && props.title !== ""
|
|
260
296
|
? props.title
|
|
261
297
|
: type.charAt(0).toUpperCase() + type.slice(1);
|
|
262
298
|
if (!children) {
|
|
@@ -291,7 +327,7 @@ const steps: ComponentMarkdown = ({ childComponents, children }) => {
|
|
|
291
327
|
return items
|
|
292
328
|
.map((step, index) => {
|
|
293
329
|
const title =
|
|
294
|
-
|
|
330
|
+
isString(step.props.title) && step.props.title !== ""
|
|
295
331
|
? `**${step.props.title}**`
|
|
296
332
|
: "";
|
|
297
333
|
const content = [title, step.children].filter(Boolean).join("\n\n");
|
|
@@ -308,7 +344,7 @@ const tabs: ComponentMarkdown = ({ childComponents, children }) => {
|
|
|
308
344
|
return items
|
|
309
345
|
.map((tab, index) => {
|
|
310
346
|
const title =
|
|
311
|
-
|
|
347
|
+
isString(tab.props.title) && tab.props.title !== ""
|
|
312
348
|
? tab.props.title
|
|
313
349
|
: `Tab ${index + 1}`;
|
|
314
350
|
return tab.children ? `**${title}**\n\n${tab.children}` : `**${title}**`;
|
|
@@ -318,9 +354,9 @@ const tabs: ComponentMarkdown = ({ childComponents, children }) => {
|
|
|
318
354
|
|
|
319
355
|
const youtube: ComponentMarkdown = ({ props }) => {
|
|
320
356
|
let input = "";
|
|
321
|
-
if (
|
|
357
|
+
if (isString(props.id)) {
|
|
322
358
|
input = props.id;
|
|
323
|
-
} else if (
|
|
359
|
+
} else if (isString(props.url)) {
|
|
324
360
|
input = props.url;
|
|
325
361
|
}
|
|
326
362
|
const videoId = parseYouTubeId(input);
|
|
@@ -328,11 +364,11 @@ const youtube: ComponentMarkdown = ({ props }) => {
|
|
|
328
364
|
return null;
|
|
329
365
|
}
|
|
330
366
|
const start =
|
|
331
|
-
|
|
367
|
+
isNumber(props.start) && props.start > 0
|
|
332
368
|
? `&t=${Math.floor(props.start)}s`
|
|
333
369
|
: "";
|
|
334
370
|
const title =
|
|
335
|
-
|
|
371
|
+
isString(props.title) && props.title !== ""
|
|
336
372
|
? props.title
|
|
337
373
|
: "Watch on YouTube";
|
|
338
374
|
return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
|
|
@@ -355,28 +391,27 @@ const fencedBlock = (lang: string, code: string): string => {
|
|
|
355
391
|
* the JSX verbatim, mirroring the "no example found" note the component renders
|
|
356
392
|
* on the page.
|
|
357
393
|
*/
|
|
358
|
-
export const exampleComponentSerializers = (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
},
|
|
366
|
-
});
|
|
394
|
+
export const exampleComponentSerializers = (examples: ExampleLookup) =>
|
|
395
|
+
({
|
|
396
|
+
Component: ({ props }) => {
|
|
397
|
+
const path = isString(props.path) ? props.path : undefined;
|
|
398
|
+
const example = path === undefined ? undefined : examples[path];
|
|
399
|
+
return example ? fencedBlock(example.lang, example.source) : null;
|
|
400
|
+
},
|
|
401
|
+
}) satisfies Record<string, ComponentMarkdown>;
|
|
367
402
|
|
|
368
403
|
/**
|
|
369
404
|
* The built-in serializer registry, keyed by JSX name. `Step` and `Tab` are
|
|
370
405
|
* intentionally absent: they only carry meaning inside their containers,
|
|
371
406
|
* which extract them via `childComponents`; a stray one stays verbatim.
|
|
372
407
|
*/
|
|
373
|
-
const SERIALIZERS
|
|
408
|
+
const SERIALIZERS = {
|
|
374
409
|
Callout: callout,
|
|
375
410
|
Steps: steps,
|
|
376
411
|
Tabs: tabs,
|
|
377
412
|
TypeTable: typeTable,
|
|
378
413
|
YouTube: youtube,
|
|
379
|
-
}
|
|
414
|
+
} satisfies Record<string, ComponentMarkdown>;
|
|
380
415
|
|
|
381
416
|
const escapeRegExp = (value: string): string =>
|
|
382
417
|
value.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`);
|
|
@@ -394,7 +429,7 @@ const BUILT_IN_HINT = componentHint(SERIALIZERS);
|
|
|
394
429
|
|
|
395
430
|
/** One downlevel pass's inputs: the source, registry, and page metadata. */
|
|
396
431
|
interface Walk {
|
|
397
|
-
frontmatter: Record<string,
|
|
432
|
+
frontmatter: Record<string, EvaluatedValue> | undefined;
|
|
398
433
|
registry: Record<string, ComponentMarkdown>;
|
|
399
434
|
source: string;
|
|
400
435
|
}
|
|
@@ -494,7 +529,7 @@ const collectSplices = (
|
|
|
494
529
|
export const downlevelComponents = (
|
|
495
530
|
source: string,
|
|
496
531
|
components?: Record<string, ComponentMarkdown>,
|
|
497
|
-
frontmatter?: Record<string,
|
|
532
|
+
frontmatter?: Record<string, EvaluatedValue>
|
|
498
533
|
): string => {
|
|
499
534
|
const custom = components && Object.keys(components).length > 0;
|
|
500
535
|
const registry = custom ? { ...SERIALIZERS, ...components } : SERIALIZERS;
|
|
@@ -504,6 +539,8 @@ export const downlevelComponents = (
|
|
|
504
539
|
}
|
|
505
540
|
let tree: MdastNode;
|
|
506
541
|
try {
|
|
542
|
+
// SAFETY: MdastNode is a structural subset of Satteri's mdast output —
|
|
543
|
+
// every node carries `type`, and the walk reads only optional fields.
|
|
507
544
|
tree = mdxToMdast(source) as MdastNode;
|
|
508
545
|
} catch {
|
|
509
546
|
return source;
|
package/src/ai/llms.ts
CHANGED
|
@@ -24,14 +24,20 @@ const pageUrl = (route: string, site?: string, base = ""): string => {
|
|
|
24
24
|
// Drafts, hidden, and ordinary `noindex` pages are excluded. Generated API
|
|
25
25
|
// references keep crawler visibility (`noindex`) separate from LLM visibility
|
|
26
26
|
// (`ai.exclude`), and are excluded wholesale when `ai.llmsTxt.openapi` is off.
|
|
27
|
-
|
|
27
|
+
// `versions: "current"` additionally drops archived-snapshot pages — the flat
|
|
28
|
+
// llms-full.txt dump serves agents the live docs, not every frozen copy.
|
|
29
|
+
const eligiblePages = (
|
|
30
|
+
project: BlumeProject,
|
|
31
|
+
options: { versions?: "all" | "current" } = {}
|
|
32
|
+
): PageRecord[] =>
|
|
28
33
|
project.graph.pages.filter(
|
|
29
34
|
(page) =>
|
|
30
35
|
!(
|
|
31
36
|
page.meta.ai.exclude ||
|
|
32
37
|
page.meta.draft ||
|
|
33
38
|
page.meta.sidebar.hidden ||
|
|
34
|
-
(page.meta.seo.noindex && page.source.name !== "openapi")
|
|
39
|
+
(page.meta.seo.noindex && page.source.name !== "openapi") ||
|
|
40
|
+
(options.versions === "current" && page.version !== "")
|
|
35
41
|
) &&
|
|
36
42
|
(project.config.ai.llmsTxt.openapi || page.source.name !== "openapi")
|
|
37
43
|
);
|
|
@@ -39,22 +45,40 @@ const eligiblePages = (project: BlumeProject): PageRecord[] =>
|
|
|
39
45
|
/**
|
|
40
46
|
* The navigation trees the index mirrors: the site tree, or one per locale
|
|
41
47
|
* under i18n (each labeled with the locale except the default, so sections
|
|
42
|
-
* don't repeat ambiguously).
|
|
48
|
+
* don't repeat ambiguously). On a versioned site each archived snapshot's
|
|
49
|
+
* trees follow the current ones, labeled with the version (and locale) so an
|
|
50
|
+
* agent reading the index knows which docs are frozen.
|
|
43
51
|
*/
|
|
44
52
|
const indexedNavigations = (
|
|
45
53
|
project: BlumeProject
|
|
46
54
|
): { label?: string; nav: Navigation }[] => {
|
|
47
|
-
const { i18n } = project.config;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
const { i18n, versions } = project.config;
|
|
56
|
+
const current: { label?: string; nav: Navigation }[] = i18n
|
|
57
|
+
? i18n.locales.flatMap(({ code, label }) => {
|
|
58
|
+
const nav = project.graph.navigationByLocale[code];
|
|
59
|
+
if (!nav) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
{ label: code === i18n.defaultLocale ? undefined : label, nav },
|
|
64
|
+
];
|
|
65
|
+
})
|
|
66
|
+
: [{ nav: project.graph.navigation }];
|
|
67
|
+
|
|
68
|
+
const archived = (versions?.archived ?? []).flatMap((version) => {
|
|
69
|
+
const byLocale = project.graph.navigationByVersion[version.id] ?? {};
|
|
70
|
+
const versionLabel = `${version.label ?? version.id} (archived)`;
|
|
71
|
+
return Object.entries(byLocale).flatMap(([code, nav]) => {
|
|
72
|
+
const locale = i18n?.locales.find((entry) => entry.code === code);
|
|
73
|
+
const label =
|
|
74
|
+
locale && code !== i18n?.defaultLocale
|
|
75
|
+
? `${locale.label} — ${versionLabel}`
|
|
76
|
+
: versionLabel;
|
|
77
|
+
return [{ label, nav }];
|
|
55
78
|
});
|
|
56
|
-
}
|
|
57
|
-
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return [...current, ...archived];
|
|
58
82
|
};
|
|
59
83
|
|
|
60
84
|
/**
|
|
@@ -169,11 +193,11 @@ export const buildLlmsIndex = (project: BlumeProject): string => {
|
|
|
169
193
|
return `${[header, ...blocks].join("\n\n")}\n`;
|
|
170
194
|
};
|
|
171
195
|
|
|
172
|
-
/** Build `llms-full.txt`: the full Markdown body of every page. */
|
|
196
|
+
/** Build `llms-full.txt`: the full Markdown body of every current-docs page. */
|
|
173
197
|
const buildFull = async (project: BlumeProject): Promise<string> => {
|
|
174
198
|
const { config } = project;
|
|
175
|
-
const pages = eligiblePages(project).toSorted(
|
|
176
|
-
a.route.localeCompare(b.route)
|
|
199
|
+
const pages = eligiblePages(project, { versions: "current" }).toSorted(
|
|
200
|
+
(a, b) => a.route.localeCompare(b.route)
|
|
177
201
|
);
|
|
178
202
|
// Downlevel `<Component>` to its example's source; a same-name user
|
|
179
203
|
// `markdownComponents` entry is spread last and still wins.
|
package/src/ai/mcp/data.ts
CHANGED
|
@@ -14,8 +14,12 @@ export interface McpRoute {
|
|
|
14
14
|
facets?: Record<string, string>;
|
|
15
15
|
indexable: boolean;
|
|
16
16
|
lastModified: string | null;
|
|
17
|
+
/** Resolved locale code (the default locale when not under i18n). */
|
|
18
|
+
locale: string;
|
|
17
19
|
route: string;
|
|
18
20
|
title: string;
|
|
21
|
+
/** Docs version (`""` for the current docs). */
|
|
22
|
+
version: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -37,10 +41,20 @@ export interface McpData {
|
|
|
37
41
|
* `search_docs` can match CJK/Thai content.
|
|
38
42
|
*/
|
|
39
43
|
defaultLocale?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Archived docs version ids, in configured order. Present only on a
|
|
46
|
+
* versioned site; its presence is what makes `search_docs`/`list_pages`
|
|
47
|
+
* default to the current docs.
|
|
48
|
+
*/
|
|
49
|
+
archivedVersions?: string[];
|
|
40
50
|
documents: OramaDoc[];
|
|
41
51
|
instructions?: string;
|
|
42
52
|
name: string;
|
|
43
53
|
navigation: Navigation;
|
|
54
|
+
/** Per-locale trees for a locale-aware `get_navigation` (i18n sites only). */
|
|
55
|
+
navigationByLocale?: Record<string, Navigation>;
|
|
56
|
+
/** Per-archived-version trees, keyed by version id then locale code. */
|
|
57
|
+
navigationByVersion?: Record<string, Record<string, Navigation>>;
|
|
44
58
|
pages: Record<string, string>;
|
|
45
59
|
routes: McpRoute[];
|
|
46
60
|
site: string | null;
|
|
@@ -79,28 +93,40 @@ export const buildMcpData = async (project: BlumeProject): Promise<McpData> => {
|
|
|
79
93
|
}
|
|
80
94
|
const page = pageById.get(route.id);
|
|
81
95
|
const facets = page ? pageFacets(page, config) : undefined;
|
|
82
|
-
|
|
96
|
+
const entry: McpRoute = {
|
|
83
97
|
contentType: route.contentType,
|
|
84
98
|
description: page?.description,
|
|
85
|
-
...(facets ? { facets } : {}),
|
|
86
99
|
indexable: route.indexable,
|
|
87
100
|
lastModified: route.lastModified ?? null,
|
|
101
|
+
locale: route.locale,
|
|
88
102
|
route: route.path,
|
|
89
103
|
title: route.title,
|
|
90
|
-
|
|
104
|
+
version: route.version,
|
|
105
|
+
};
|
|
106
|
+
if (facets) {
|
|
107
|
+
entry.facets = facets;
|
|
108
|
+
}
|
|
109
|
+
routes.push(entry);
|
|
91
110
|
}
|
|
92
111
|
|
|
93
|
-
|
|
112
|
+
const data: McpData = {
|
|
94
113
|
base: normalizeBasePath(config.deployment.base),
|
|
95
114
|
defaultLocale: config.i18n?.defaultLocale,
|
|
96
|
-
documents: documents.map((doc) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
documents: documents.map((doc) => {
|
|
116
|
+
const document: OramaDoc = {
|
|
117
|
+
content: doc.content,
|
|
118
|
+
contentType: doc.contentType,
|
|
119
|
+
description: doc.description,
|
|
120
|
+
locale: doc.locale,
|
|
121
|
+
route: doc.route,
|
|
122
|
+
title: doc.title,
|
|
123
|
+
version: doc.version,
|
|
124
|
+
};
|
|
125
|
+
if (doc.facets) {
|
|
126
|
+
document.facets = doc.facets;
|
|
127
|
+
}
|
|
128
|
+
return document;
|
|
129
|
+
}),
|
|
104
130
|
instructions: config.ai.mcp.instructions,
|
|
105
131
|
name: config.ai.mcp.name ?? config.title,
|
|
106
132
|
navigation: graph.navigation,
|
|
@@ -109,4 +135,14 @@ export const buildMcpData = async (project: BlumeProject): Promise<McpData> => {
|
|
|
109
135
|
site: config.deployment.site ?? null,
|
|
110
136
|
version: manifest.blumeVersion,
|
|
111
137
|
};
|
|
138
|
+
if (config.versions) {
|
|
139
|
+
data.archivedVersions = config.versions.archived.map(
|
|
140
|
+
(version) => version.id
|
|
141
|
+
);
|
|
142
|
+
data.navigationByVersion = graph.navigationByVersion;
|
|
143
|
+
}
|
|
144
|
+
if (config.i18n) {
|
|
145
|
+
data.navigationByLocale = graph.navigationByLocale;
|
|
146
|
+
}
|
|
147
|
+
return data;
|
|
112
148
|
};
|
package/src/ai/mcp/discovery.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { withBasePath } from "../../core/base-path.ts";
|
|
|
2
2
|
import { absoluteUrl, siteRoot } from "../../core/site-url.ts";
|
|
3
3
|
import { trimChar } from "../../core/trim.ts";
|
|
4
4
|
import { MCP_TOOLS } from "./tools.ts";
|
|
5
|
+
import type { McpToolMeta } from "./tools.ts";
|
|
5
6
|
|
|
6
7
|
/** Inputs needed to describe the MCP server in discovery documents. */
|
|
7
8
|
export interface McpDiscoveryInput {
|
|
@@ -27,9 +28,7 @@ const serverUrl = (input: McpDiscoveryInput): string => {
|
|
|
27
28
|
* The `/.well-known/mcp.json` discovery document: the minimal pointer agents use
|
|
28
29
|
* to find the server and its transport.
|
|
29
30
|
*/
|
|
30
|
-
export const buildMcpDiscovery = (
|
|
31
|
-
input: McpDiscoveryInput
|
|
32
|
-
): Record<string, unknown> => ({
|
|
31
|
+
export const buildMcpDiscovery = (input: McpDiscoveryInput) => ({
|
|
33
32
|
servers: [
|
|
34
33
|
{
|
|
35
34
|
name: input.name,
|
|
@@ -102,20 +101,32 @@ const HTTP_URL = /^https?:\/\//u;
|
|
|
102
101
|
* excludes primitives for. Full input schemas are served live via
|
|
103
102
|
* `tools/list`.
|
|
104
103
|
*/
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
export interface McpServerCard {
|
|
105
|
+
$schema: string;
|
|
106
|
+
capabilities: { tools: { listChanged: boolean } };
|
|
107
|
+
description: string;
|
|
108
|
+
name: string;
|
|
109
|
+
/** Absolute endpoints only — present when a `site` is configured. */
|
|
110
|
+
remotes?: { type: string; url: string }[];
|
|
111
|
+
serverInfo: { name: string; version: string };
|
|
112
|
+
title: string;
|
|
113
|
+
tools: McpToolMeta[];
|
|
114
|
+
transport: string;
|
|
115
|
+
transports: { endpoint: string; type: string }[];
|
|
116
|
+
url: string;
|
|
117
|
+
version: string;
|
|
118
|
+
websiteUrl?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const buildMcpServerCard = (input: McpDiscoveryInput): McpServerCard => {
|
|
108
122
|
const url = serverUrl(input);
|
|
109
|
-
|
|
123
|
+
const card: McpServerCard = {
|
|
110
124
|
$schema: SERVER_CARD_SCHEMA,
|
|
111
125
|
capabilities: { tools: { listChanged: false } },
|
|
112
126
|
description: truncate(
|
|
113
127
|
`Model Context Protocol server for the ${input.name} documentation.`
|
|
114
128
|
),
|
|
115
129
|
name: reverseDnsName(input),
|
|
116
|
-
...(HTTP_URL.test(url)
|
|
117
|
-
? { remotes: [{ type: "streamable-http", url }] }
|
|
118
|
-
: {}),
|
|
119
130
|
serverInfo: { name: input.name, version: input.version },
|
|
120
131
|
title: truncate(input.name),
|
|
121
132
|
tools: MCP_TOOLS.map((tool) => ({
|
|
@@ -128,6 +139,12 @@ export const buildMcpServerCard = (
|
|
|
128
139
|
transports: [{ endpoint: url, type: "streamable-http" }],
|
|
129
140
|
url,
|
|
130
141
|
version: input.version,
|
|
131
|
-
...(input.site ? { websiteUrl: siteRoot(input.site) } : {}),
|
|
132
142
|
};
|
|
143
|
+
if (HTTP_URL.test(url)) {
|
|
144
|
+
card.remotes = [{ type: "streamable-http", url }];
|
|
145
|
+
}
|
|
146
|
+
if (input.site) {
|
|
147
|
+
card.websiteUrl = siteRoot(input.site);
|
|
148
|
+
}
|
|
149
|
+
return card;
|
|
133
150
|
};
|