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.
Files changed (194) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/index.js +1621 -576
  3. package/dist/cli/index.js.map +109 -104
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/config-input.d.ts +79 -27
  6. package/dist/types/core/config.d.ts +2 -1
  7. package/dist/types/core/data.d.ts +16 -1
  8. package/dist/types/core/diagnostics.d.ts +5 -1
  9. package/dist/types/core/i18n-ui.d.ts +12 -0
  10. package/dist/types/core/schema.d.ts +112 -15
  11. package/dist/types/core/sources/types.d.ts +3 -1
  12. package/dist/types/core/standard-schema.d.ts +7 -3
  13. package/dist/types/core/types.d.ts +43 -2
  14. package/dist/types/core/ui-packs/index.d.ts +9 -1
  15. package/dist/types/openapi/references.d.ts +6 -5
  16. package/dist/types/seo/x-handle.d.ts +3 -2
  17. package/docs/advanced/api-reference.mdx +8 -6
  18. package/docs/configuration/search.mdx +2 -0
  19. package/docs/configuration/seo.mdx +1 -1
  20. package/docs/content/i18n.mdx +1 -1
  21. package/docs/content/meta.mdx +2 -1
  22. package/docs/content/meta.ts +1 -0
  23. package/docs/content/navigation.mdx +35 -1
  24. package/docs/content/versioning.mdx +106 -0
  25. package/docs/reference/cli.mdx +1 -0
  26. package/docs/reference/frontmatter.mdx +3 -0
  27. package/package.json +3 -1
  28. package/skills/blume-migrate/SKILL.md +2 -2
  29. package/skills/blume-migrate/references/docusaurus.md +1 -1
  30. package/skills/blume-migrate/references/fumadocs.md +1 -1
  31. package/skills/blume-migrate/references/mintlify.md +1 -1
  32. package/src/ai/agent-readability.ts +37 -10
  33. package/src/ai/ask-context.ts +5 -1
  34. package/src/ai/ask.ts +10 -1
  35. package/src/ai/component-markdown.ts +80 -43
  36. package/src/ai/llms.ts +40 -16
  37. package/src/ai/mcp/data.ts +48 -12
  38. package/src/ai/mcp/discovery.ts +28 -11
  39. package/src/ai/mcp/server.ts +183 -38
  40. package/src/ai/mcp/tools.ts +3 -3
  41. package/src/ai/skills.ts +32 -9
  42. package/src/ai/visibility.ts +2 -2
  43. package/src/astro/component-slots.ts +2 -0
  44. package/src/astro/examples.ts +6 -2
  45. package/src/astro/generate.ts +54 -29
  46. package/src/astro/integration.ts +13 -2
  47. package/src/astro/islands.ts +16 -9
  48. package/src/astro/templates.ts +152 -33
  49. package/src/audit/agent.ts +2 -2
  50. package/src/audit/checks/content.ts +26 -11
  51. package/src/audit/checks/dns-aid.ts +3 -0
  52. package/src/audit/checks/indexability.ts +24 -6
  53. package/src/audit/checks/llms.ts +9 -4
  54. package/src/audit/checks/network.ts +2 -0
  55. package/src/audit/checks/social.ts +18 -10
  56. package/src/audit/crawl.ts +37 -9
  57. package/src/audit/report.ts +20 -19
  58. package/src/audit/run.ts +5 -2
  59. package/src/audit/snapshot.ts +2 -4
  60. package/src/audit/types.ts +25 -3
  61. package/src/blume-modules.d.ts +5 -1
  62. package/src/cli/commands/audit.ts +9 -4
  63. package/src/cli/commands/build.ts +15 -9
  64. package/src/cli/commands/dev.ts +2 -0
  65. package/src/cli/commands/doctor.ts +2 -0
  66. package/src/cli/commands/eval.ts +7 -3
  67. package/src/cli/commands/init.ts +9 -9
  68. package/src/cli/commands/mcp-stdio.ts +3 -0
  69. package/src/cli/commands/translate.ts +14 -3
  70. package/src/cli/commands/version.ts +85 -0
  71. package/src/cli/dev-lock.ts +31 -10
  72. package/src/cli/eject-scripts.ts +17 -2
  73. package/src/cli/index.ts +2 -0
  74. package/src/cli/init/questions.ts +1 -1
  75. package/src/cli/init/scaffold.ts +22 -15
  76. package/src/cli/internal-error.ts +1 -0
  77. package/src/components/content/auto-type-table.ts +3 -0
  78. package/src/components/content/diff.ts +9 -5
  79. package/src/components/content/github-info.ts +2 -0
  80. package/src/components/islands/ask-ai.tsx +33 -25
  81. package/src/components/islands/hooks.ts +5 -1
  82. package/src/components/islands/webmcp.ts +49 -12
  83. package/src/components/layout/Header.astro +25 -1
  84. package/src/components/layout/NavSelector.astro +11 -2
  85. package/src/components/layout/NavTree.astro +4 -2
  86. package/src/components/layout/RootLayout.astro +18 -0
  87. package/src/components/layout/Search.astro +77 -13
  88. package/src/components/layout/VersionBanner.astro +39 -0
  89. package/src/components/layout/analytics-client.ts +8 -5
  90. package/src/components/layout/hydration-hint.ts +1 -1
  91. package/src/components/layout/nav-utils.ts +1 -4
  92. package/src/components/layout/overrides.ts +25 -12
  93. package/src/components/layout/search/algolia.ts +18 -5
  94. package/src/components/layout/search/endpoint.ts +3 -0
  95. package/src/components/layout/search/flexsearch.ts +23 -7
  96. package/src/components/layout/search/orama-cloud.ts +1 -1
  97. package/src/components/layout/search/orama.ts +4 -1
  98. package/src/components/layout/search/pagefind.ts +2 -0
  99. package/src/components/layout/search/types.ts +13 -1
  100. package/src/components/layout/search/typesense.ts +19 -3
  101. package/src/components/openapi/ApiOverview.astro +32 -6
  102. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  103. package/src/components/openapi/Bindings.astro +89 -0
  104. package/src/components/openapi/MethodBadge.astro +3 -0
  105. package/src/components/openapi/Operation.astro +7 -2
  106. package/src/components/openapi/PanelTabs.astro +131 -0
  107. package/src/components/openapi/ParametersTable.astro +2 -0
  108. package/src/components/openapi/RequestPanel.astro +12 -119
  109. package/src/components/openapi/async-snippets.ts +174 -0
  110. package/src/components/openapi/async.ts +348 -0
  111. package/src/components/openapi/helpers.ts +52 -20
  112. package/src/components/openapi/security.ts +102 -29
  113. package/src/components/openapi/snippets.ts +11 -11
  114. package/src/core/component-overrides.ts +28 -23
  115. package/src/core/config-input.ts +88 -27
  116. package/src/core/config.ts +20 -7
  117. package/src/core/content.ts +3 -1
  118. package/src/core/data.ts +16 -1
  119. package/src/core/define-components.ts +5 -0
  120. package/src/core/diagnostics.ts +46 -38
  121. package/src/core/frontmatter.ts +33 -7
  122. package/src/core/graph.ts +137 -53
  123. package/src/core/i18n-ui.ts +15 -0
  124. package/src/core/i18n.ts +16 -8
  125. package/src/core/load-module.ts +1 -0
  126. package/src/core/manifest.ts +92 -3
  127. package/src/core/meta.ts +44 -14
  128. package/src/core/nav-diagnostics.ts +3 -3
  129. package/src/core/navigation.ts +247 -67
  130. package/src/core/project-graph.ts +15 -3
  131. package/src/core/schema.ts +213 -67
  132. package/src/core/sources/assets.ts +2 -0
  133. package/src/core/sources/cache.ts +6 -0
  134. package/src/core/sources/github-releases.ts +39 -31
  135. package/src/core/sources/mdx-remote.ts +4 -0
  136. package/src/core/sources/normalize.ts +67 -20
  137. package/src/core/sources/notion.ts +49 -17
  138. package/src/core/sources/portable-text.ts +32 -11
  139. package/src/core/sources/sanity.ts +68 -14
  140. package/src/core/sources/types.ts +4 -0
  141. package/src/core/sources/watch.ts +1 -1
  142. package/src/core/standard-schema.ts +9 -3
  143. package/src/core/text-width.ts +26 -0
  144. package/src/core/tsconfig-aliases.ts +9 -5
  145. package/src/core/types.ts +45 -2
  146. package/src/core/ui-packs/index.ts +9 -1
  147. package/src/core/version-cut.ts +301 -0
  148. package/src/core/version.ts +2 -0
  149. package/src/core/versions.ts +170 -0
  150. package/src/deploy/adapter-output.ts +5 -2
  151. package/src/deploy/cloudflare-negotiation.ts +25 -10
  152. package/src/deploy/sitemap.ts +33 -1
  153. package/src/deploy/vercel-negotiation.ts +11 -4
  154. package/src/eval/report.ts +4 -4
  155. package/src/eval/run.ts +2 -2
  156. package/src/eval/schema.ts +1 -1
  157. package/src/markdown/base-links.ts +6 -6
  158. package/src/markdown/directives.ts +7 -1
  159. package/src/markdown/heading-anchors.ts +17 -6
  160. package/src/markdown/index.ts +73 -24
  161. package/src/markdown/inline-code.ts +14 -2
  162. package/src/markdown/language-icon.ts +6 -2
  163. package/src/markdown/mdast.ts +18 -4
  164. package/src/markdown/package-commands.ts +6 -8
  165. package/src/markdown/table-wrap.ts +4 -1
  166. package/src/markdown/twoslash.ts +2 -0
  167. package/src/og/card.ts +30 -11
  168. package/src/og/derive.ts +43 -27
  169. package/src/openapi/asyncapi.ts +366 -0
  170. package/src/openapi/model.ts +126 -57
  171. package/src/openapi/parse.ts +97 -5
  172. package/src/openapi/references.ts +12 -10
  173. package/src/openapi/render-mdx.ts +73 -34
  174. package/src/openapi/scalar.ts +6 -8
  175. package/src/openapi/source.ts +98 -28
  176. package/src/registry/eject.ts +7 -2
  177. package/src/search/documents.ts +25 -5
  178. package/src/search/facets.ts +7 -5
  179. package/src/search/orama-index.ts +66 -20
  180. package/src/search/popular.ts +10 -5
  181. package/src/search/providers.ts +2 -2
  182. package/src/search/sync/index.ts +2 -0
  183. package/src/search/sync/typesense.ts +4 -2
  184. package/src/seo/jsonld.ts +24 -6
  185. package/src/seo/x-handle.ts +8 -3
  186. package/src/theme/chrome-icons.ts +7 -2
  187. package/src/theme/fonts.ts +8 -4
  188. package/src/theme/icons.ts +4 -2
  189. package/src/theme/palette.ts +22 -14
  190. package/src/translate/meta.ts +15 -6
  191. package/src/translate/report.ts +9 -5
  192. package/src/translate/run.ts +10 -4
  193. package/src/translate/validate.ts +52 -17
  194. 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, unknown>;
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, unknown>;
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, unknown> | undefined
97
- ): { ok: boolean; value: unknown } => {
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("frontmatter", `"use strict"; return (${raw});`)(
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, unknown> | undefined
136
+ frontmatter: Record<string, EvaluatedValue> | undefined
115
137
  ): EvaluatedProps => {
116
- const props: Record<string, unknown> = {};
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 (typeof attribute.value === "string") {
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: unknown): string =>
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: unknown): string => {
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
- /** One `<TypeTable type={{...}}>` entry, matching the component's shape. */
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
- default?: unknown;
216
- description?: unknown;
217
- required?: unknown;
218
- type?: unknown;
219
- typeDescription?: unknown;
220
- typeDescriptionLink?: unknown;
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 === null || typeof type !== "object") {
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 as Record<string, TypeEntry>);
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) => typeof part === "string" && 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 = typeof props.type === "string" ? props.type : "info";
293
+ const type = isString(props.type) ? props.type : "info";
258
294
  const label =
259
- typeof props.title === "string" && props.title !== ""
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
- typeof step.props.title === "string" && step.props.title !== ""
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
- typeof tab.props.title === "string" && tab.props.title !== ""
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 (typeof props.id === "string") {
357
+ if (isString(props.id)) {
322
358
  input = props.id;
323
- } else if (typeof props.url === "string") {
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
- typeof props.start === "number" && props.start > 0
367
+ isNumber(props.start) && props.start > 0
332
368
  ? `&t=${Math.floor(props.start)}s`
333
369
  : "";
334
370
  const title =
335
- typeof props.title === "string" && props.title !== ""
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
- examples: ExampleLookup
360
- ): Record<string, ComponentMarkdown> => ({
361
- Component: ({ props }) => {
362
- const path = typeof props.path === "string" ? props.path : undefined;
363
- const example = path === undefined ? undefined : examples[path];
364
- return example ? fencedBlock(example.lang, example.source) : null;
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: Record<string, ComponentMarkdown> = {
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, unknown> | undefined;
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, unknown>
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
- const eligiblePages = (project: BlumeProject): PageRecord[] =>
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
- if (i18n) {
49
- return i18n.locales.flatMap(({ code, label }) => {
50
- const nav = project.graph.navigationByLocale[code];
51
- if (!nav) {
52
- return [];
53
- }
54
- return [{ label: code === i18n.defaultLocale ? undefined : label, nav }];
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
- return [{ nav: project.graph.navigation }];
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((a, b) =>
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.
@@ -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
- routes.push({
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
- return {
112
+ const data: McpData = {
94
113
  base: normalizeBasePath(config.deployment.base),
95
114
  defaultLocale: config.i18n?.defaultLocale,
96
- documents: documents.map((doc) => ({
97
- content: doc.content,
98
- contentType: doc.contentType,
99
- description: doc.description,
100
- ...(doc.facets ? { facets: doc.facets } : {}),
101
- route: doc.route,
102
- title: doc.title,
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
  };
@@ -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 const buildMcpServerCard = (
106
- input: McpDiscoveryInput
107
- ): Record<string, unknown> => {
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
- return {
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
  };