blume 1.7.0 → 1.7.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli/{chunk-9qs6acpw.js → chunk-12dxjqk7.js} +32 -43
  3. package/dist/cli/{chunk-9qs6acpw.js.map → chunk-12dxjqk7.js.map} +2 -2
  4. package/dist/cli/{chunk-s5dsk8bj.js → chunk-196vjxp9.js} +65 -64
  5. package/dist/cli/chunk-196vjxp9.js.map +13 -0
  6. package/dist/cli/{chunk-27gtm2ym.js → chunk-2mzebbbz.js} +1 -1
  7. package/dist/cli/{chunk-s5e5jt53.js → chunk-2z47ypj8.js} +1 -1
  8. package/dist/cli/{chunk-tnskyrej.js → chunk-30e87n55.js} +15 -24
  9. package/dist/cli/{chunk-tnskyrej.js.map → chunk-30e87n55.js.map} +2 -2
  10. package/dist/cli/{chunk-4trphnvy.js → chunk-3w7b2vcx.js} +10 -13
  11. package/dist/cli/{chunk-4trphnvy.js.map → chunk-3w7b2vcx.js.map} +2 -2
  12. package/dist/cli/{chunk-v5mm027v.js → chunk-450a7rcr.js} +8 -8
  13. package/dist/cli/{chunk-v5mm027v.js.map → chunk-450a7rcr.js.map} +1 -1
  14. package/dist/cli/{chunk-5d4q7121.js → chunk-5n7t497w.js} +142 -134
  15. package/dist/cli/{chunk-5d4q7121.js.map → chunk-5n7t497w.js.map} +5 -5
  16. package/dist/cli/{chunk-xv91q4nm.js → chunk-61j18dwk.js} +37 -9
  17. package/dist/cli/{chunk-xv91q4nm.js.map → chunk-61j18dwk.js.map} +3 -3
  18. package/dist/cli/{chunk-3r94j3tc.js → chunk-688e0dde.js} +2 -2
  19. package/dist/cli/{chunk-vxv4x1n8.js → chunk-88by27n5.js} +2 -2
  20. package/dist/cli/{chunk-cfw6x4rm.js → chunk-8cd8tj54.js} +28 -35
  21. package/dist/cli/{chunk-cfw6x4rm.js.map → chunk-8cd8tj54.js.map} +2 -2
  22. package/dist/cli/{chunk-8gnpdsn1.js → chunk-9bkjd11x.js} +2 -2
  23. package/dist/cli/{chunk-qq9nm3qd.js → chunk-9he6crym.js} +21 -28
  24. package/dist/cli/{chunk-qq9nm3qd.js.map → chunk-9he6crym.js.map} +2 -2
  25. package/dist/cli/{chunk-jk1zwka1.js → chunk-aztttvb3.js} +27 -33
  26. package/dist/cli/{chunk-jk1zwka1.js.map → chunk-aztttvb3.js.map} +2 -2
  27. package/dist/cli/{chunk-ckh3a410.js → chunk-cvky9gb2.js} +18 -24
  28. package/dist/cli/{chunk-ckh3a410.js.map → chunk-cvky9gb2.js.map} +2 -2
  29. package/dist/cli/{chunk-kwx90v78.js → chunk-eevwt1sc.js} +23 -32
  30. package/dist/cli/{chunk-kwx90v78.js.map → chunk-eevwt1sc.js.map} +2 -2
  31. package/dist/cli/{chunk-agy5rzxy.js → chunk-ejjx8znq.js} +126 -77
  32. package/dist/cli/chunk-ejjx8znq.js.map +15 -0
  33. package/dist/cli/{chunk-ye9zdkgv.js → chunk-exeeb35e.js} +3 -3
  34. package/dist/cli/{chunk-v2ymm99c.js → chunk-fmceyezb.js} +41 -50
  35. package/dist/cli/{chunk-v2ymm99c.js.map → chunk-fmceyezb.js.map} +2 -2
  36. package/dist/cli/{chunk-s102bysw.js → chunk-hdm2dkd2.js} +209 -97
  37. package/dist/cli/{chunk-s102bysw.js.map → chunk-hdm2dkd2.js.map} +6 -5
  38. package/dist/cli/{chunk-zr3ygrq3.js → chunk-hs3gbh8p.js} +10 -15
  39. package/dist/cli/{chunk-zr3ygrq3.js.map → chunk-hs3gbh8p.js.map} +2 -2
  40. package/dist/cli/{chunk-n0nyat6g.js → chunk-jbj4qhfw.js} +3 -3
  41. package/dist/cli/{chunk-ev67ycx0.js → chunk-jq5n4avg.js} +1 -1
  42. package/dist/cli/{chunk-jxkxjsc1.js → chunk-mqb2ka8m.js} +22 -30
  43. package/dist/cli/{chunk-jxkxjsc1.js.map → chunk-mqb2ka8m.js.map} +2 -2
  44. package/dist/cli/{chunk-drke6t0h.js → chunk-mt76t7dj.js} +26 -34
  45. package/dist/cli/{chunk-drke6t0h.js.map → chunk-mt76t7dj.js.map} +2 -2
  46. package/dist/cli/{chunk-jtb45atp.js → chunk-n9sra6sy.js} +13 -13
  47. package/dist/cli/{chunk-jtb45atp.js.map → chunk-n9sra6sy.js.map} +1 -1
  48. package/dist/cli/{chunk-x66c5yjn.js → chunk-ppfvdcd4.js} +2 -2
  49. package/dist/cli/{chunk-wd27zjcz.js → chunk-q4rae3bg.js} +1 -1
  50. package/dist/cli/{chunk-pxj10x8y.js → chunk-ra1v2nc2.js} +1 -1
  51. package/dist/cli/{chunk-y3g15rvv.js → chunk-t3tj0dgr.js} +26 -33
  52. package/dist/cli/{chunk-y3g15rvv.js.map → chunk-t3tj0dgr.js.map} +2 -2
  53. package/dist/cli/{chunk-j6pxe0dt.js → chunk-tqa1s0k8.js} +4 -4
  54. package/dist/cli/{chunk-cbjnx4s8.js → chunk-vh9w1sgp.js} +1 -1
  55. package/dist/cli/{chunk-0qhq7b8q.js → chunk-vkrsvbr5.js} +13 -17
  56. package/dist/cli/{chunk-0qhq7b8q.js.map → chunk-vkrsvbr5.js.map} +2 -2
  57. package/dist/cli/{chunk-sbdqrjbb.js → chunk-vrfp10qk.js} +1 -1
  58. package/dist/cli/{chunk-ynacq3ev.js → chunk-wjt80jps.js} +27 -40
  59. package/dist/cli/{chunk-ynacq3ev.js.map → chunk-wjt80jps.js.map} +3 -4
  60. package/dist/cli/{chunk-18tjv4f7.js → chunk-xhtpx3ff.js} +21 -30
  61. package/dist/cli/{chunk-18tjv4f7.js.map → chunk-xhtpx3ff.js.map} +2 -2
  62. package/dist/cli/{chunk-5hs6gb7n.js → chunk-yzhm0j9q.js} +1 -1
  63. package/dist/cli/index.js +397 -34
  64. package/dist/cli/index.js.map +12 -4
  65. package/dist/types/components/layout/nav-utils.d.ts +15 -2
  66. package/dist/types/core/config-input.d.ts +33 -0
  67. package/dist/types/core/schema.d.ts +29 -3
  68. package/docs/08-faq.mdx +21 -0
  69. package/docs/configuration/ask-ai.mdx +63 -0
  70. package/docs/content/sources.mdx +2 -2
  71. package/package.json +1 -1
  72. package/src/ai/ask.ts +31 -4
  73. package/src/ai/cors.ts +87 -0
  74. package/src/astro/generate.ts +3 -0
  75. package/src/astro/templates.ts +192 -83
  76. package/src/components/content/YouTube.astro +1 -1
  77. package/src/components/layout/Header.astro +1 -1
  78. package/src/components/layout/NavTree.astro +75 -66
  79. package/src/components/layout/RootLayout.astro +4 -1
  80. package/src/components/layout/Search.astro +11 -0
  81. package/src/components/layout/nav-utils.ts +47 -15
  82. package/src/core/adapter.ts +61 -0
  83. package/src/core/config-input.ts +33 -0
  84. package/src/core/schema.ts +61 -0
  85. package/src/core/sources/assets.ts +162 -26
  86. package/src/core/sources/notion.ts +60 -1
  87. package/src/registry/eject.ts +3 -0
  88. package/src/theme/entry.ts +9 -0
  89. package/dist/cli/chunk-2aj8ddew.js +0 -72
  90. package/dist/cli/chunk-2aj8ddew.js.map +0 -10
  91. package/dist/cli/chunk-4xyggvgf.js +0 -21
  92. package/dist/cli/chunk-4xyggvgf.js.map +0 -10
  93. package/dist/cli/chunk-6kzzpsx8.js +0 -26
  94. package/dist/cli/chunk-6kzzpsx8.js.map +0 -10
  95. package/dist/cli/chunk-agy5rzxy.js.map +0 -15
  96. package/dist/cli/chunk-bcy492zc.js +0 -16
  97. package/dist/cli/chunk-bcy492zc.js.map +0 -10
  98. package/dist/cli/chunk-btfr9yvw.js +0 -41
  99. package/dist/cli/chunk-btfr9yvw.js.map +0 -10
  100. package/dist/cli/chunk-ey89bjj1.js +0 -209
  101. package/dist/cli/chunk-ey89bjj1.js.map +0 -11
  102. package/dist/cli/chunk-s5dsk8bj.js.map +0 -13
  103. package/dist/cli/chunk-vt8fgygt.js +0 -23
  104. package/dist/cli/chunk-vt8fgygt.js.map +0 -10
  105. /package/dist/cli/{chunk-27gtm2ym.js.map → chunk-2mzebbbz.js.map} +0 -0
  106. /package/dist/cli/{chunk-s5e5jt53.js.map → chunk-2z47ypj8.js.map} +0 -0
  107. /package/dist/cli/{chunk-3r94j3tc.js.map → chunk-688e0dde.js.map} +0 -0
  108. /package/dist/cli/{chunk-vxv4x1n8.js.map → chunk-88by27n5.js.map} +0 -0
  109. /package/dist/cli/{chunk-8gnpdsn1.js.map → chunk-9bkjd11x.js.map} +0 -0
  110. /package/dist/cli/{chunk-ye9zdkgv.js.map → chunk-exeeb35e.js.map} +0 -0
  111. /package/dist/cli/{chunk-n0nyat6g.js.map → chunk-jbj4qhfw.js.map} +0 -0
  112. /package/dist/cli/{chunk-ev67ycx0.js.map → chunk-jq5n4avg.js.map} +0 -0
  113. /package/dist/cli/{chunk-x66c5yjn.js.map → chunk-ppfvdcd4.js.map} +0 -0
  114. /package/dist/cli/{chunk-wd27zjcz.js.map → chunk-q4rae3bg.js.map} +0 -0
  115. /package/dist/cli/{chunk-pxj10x8y.js.map → chunk-ra1v2nc2.js.map} +0 -0
  116. /package/dist/cli/{chunk-j6pxe0dt.js.map → chunk-tqa1s0k8.js.map} +0 -0
  117. /package/dist/cli/{chunk-cbjnx4s8.js.map → chunk-vh9w1sgp.js.map} +0 -0
  118. /package/dist/cli/{chunk-sbdqrjbb.js.map → chunk-vrfp10qk.js.map} +0 -0
  119. /package/dist/cli/{chunk-5hs6gb7n.js.map → chunk-yzhm0j9q.js.map} +0 -0
@@ -9,7 +9,7 @@ import type { AskBackend } from "../ai/ask.ts";
9
9
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
10
10
  import { normalizeBasePath } from "../core/base-path.ts";
11
11
  import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
12
- import type { ResolvedConfig } from "../core/schema.ts";
12
+ import type { AskReasoning, ResolvedConfig } from "../core/schema.ts";
13
13
  import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
14
14
  import { trimChar } from "../core/trim.ts";
15
15
  import type { ProjectContext } from "../core/types.ts";
@@ -1040,12 +1040,58 @@ const ASK_FALLBACK_PROMPT =
1040
1040
 
1041
1041
  /** The `ai.ask` values the generated endpoint has to carry with it. */
1042
1042
  export interface AskEndpointOptions {
1043
+ /** `ai.ask.cors` — origins allowed to call the route from another site. */
1044
+ cors?: string[];
1043
1045
  /** `ai.ask.instructions` — extra system-prompt text. */
1044
1046
  instructions?: string;
1047
+ /**
1048
+ * `ai.ask.reasoning` — how much the model reasons before answering, sent
1049
+ * as the backend's own reasoning-effort control.
1050
+ */
1051
+ reasoning?: AskReasoning;
1045
1052
  /** `ai.ask.retrieval` — how much documentation each question carries. */
1046
1053
  retrieval?: AskRetrievalOptions;
1047
1054
  }
1048
1055
 
1056
+ /** The pieces `askEndpointTemplate` splices in for `ai.ask.cors`. */
1057
+ interface AskCorsTemplate {
1058
+ /** The route's closing token: `});` when the POST is wrapped, `};` otherwise. */
1059
+ close: string;
1060
+ /** The runtime import, when anything is listed. */
1061
+ imports: string[];
1062
+ /** The opening of `export const POST: APIRoute = `. */
1063
+ open: string;
1064
+ /** The allow list and the `OPTIONS` handler, spliced after the provider setup. */
1065
+ setup: string;
1066
+ }
1067
+
1068
+ /**
1069
+ * `ai.ask.cors`: a browser only lets another origin read the stream when the
1070
+ * response names that origin, and a JSON POST preflights first, so the route
1071
+ * answers `OPTIONS` and wraps the `POST` in `withCors`, which stamps a listed
1072
+ * origin on every response — errors included, so a cross-origin caller can
1073
+ * tell a 400 from a 500 — without each `return` having to remember to.
1074
+ * Unlisted origins get no allow header and stay subject to the same-origin
1075
+ * rule. Left out entirely when nothing is listed, so the default route is
1076
+ * unchanged.
1077
+ */
1078
+ const askCorsTemplate = (cors: readonly string[] = []): AskCorsTemplate =>
1079
+ cors.length > 0
1080
+ ? {
1081
+ close: "});",
1082
+ imports: [
1083
+ 'import { preflightResponse, withCors } from "blume/ai/cors.ts";',
1084
+ ],
1085
+ open: "withCors(ALLOWED_ORIGINS, async ({ request }) => {",
1086
+ setup: `
1087
+ const ALLOWED_ORIGINS = ${JSON.stringify(cors)};
1088
+
1089
+ export const OPTIONS: APIRoute = ({ request }) =>
1090
+ preflightResponse(request, ALLOWED_ORIGINS);
1091
+ `,
1092
+ }
1093
+ : { close: "};", imports: [], open: "async ({ request }) => {", setup: "" };
1094
+
1049
1095
  /**
1050
1096
  * Generate the Ask AI server endpoint (`.blume/src/pages/api/ask.ts`).
1051
1097
  *
@@ -1053,15 +1099,24 @@ export interface AskEndpointOptions {
1053
1099
  * built-in prompt on every path: the grounded prompt via `createAskContext`,
1054
1100
  * and the plain fallback here. `options.retrieval` (the `ai.ask.retrieval`
1055
1101
  * config) is forwarded to `createAskContext` on the grounded path, where it
1056
- * sizes retrieval. Both travel in one options object so a new call site can't
1057
- * silently drop one of them.
1102
+ * sizes retrieval. `options.reasoning` (the `ai.ask.reasoning` config)
1103
+ * reaches the model call on both paths. `options.cors` (the `ai.ask.cors`
1104
+ * config) adds a preflight handler and wraps the `POST` so every response
1105
+ * names a listed origin. All four travel in one options object so a new call
1106
+ * site can't silently drop one of them.
1058
1107
  */
1059
1108
  export const askEndpointTemplate = (
1060
1109
  backend: AskBackend,
1061
1110
  grounded: boolean,
1062
1111
  options?: AskEndpointOptions
1063
1112
  ): string => {
1064
- const instructions = options?.instructions;
1113
+ const { instructions, reasoning, retrieval } = options ?? {};
1114
+ // `ai.ask.reasoning`. The gateway and OpenAI-compatible providers take it
1115
+ // from `streamText`'s top-level `reasoning` (the gateway maps it to the
1116
+ // model's own control, the OpenAI-compatible provider sends it as
1117
+ // `reasoning_effort`). OpenRouter's provider ignores that call option and
1118
+ // only reads its own model setting, so there the level rides on the model
1119
+ // as `reasoning.effort`. Omitted keeps the provider default on every path.
1065
1120
  const fallbackPrompt = instructions
1066
1121
  ? `${ASK_FALLBACK_PROMPT}\n\n${instructions}`
1067
1122
  : ASK_FALLBACK_PROMPT;
@@ -1080,24 +1135,34 @@ export const askEndpointTemplate = (
1080
1135
  ];
1081
1136
  let setup = "";
1082
1137
  let modelExpr = JSON.stringify(backend.model);
1138
+ // `ai.ask.headers`, inlined as literals: every provider factory below takes
1139
+ // the same `headers` option, so one line serves all three.
1140
+ const headersField = backend.headers
1141
+ ? `\n headers: ${JSON.stringify(backend.headers)},`
1142
+ : "";
1083
1143
  if (backend.kind === "gateway") {
1084
- setup = `\nconst gateway = createGateway({ apiKey: getSecret("AI_GATEWAY_API_KEY") });\n`;
1144
+ setup = `\nconst gateway = createGateway({
1145
+ apiKey: getSecret("AI_GATEWAY_API_KEY"),${headersField}
1146
+ });\n`;
1085
1147
  modelExpr = `gateway(${JSON.stringify(backend.model)})`;
1086
1148
  } else if (backend.kind === "openrouter") {
1087
1149
  imports.push(
1088
1150
  'import { createOpenRouter } from "@openrouter/ai-sdk-provider";'
1089
1151
  );
1090
- setup = `\nconst openrouter = createOpenRouter({ apiKey: getSecret(${JSON.stringify(
1091
- backend.apiKeyEnv
1092
- )}) });\n`;
1093
- modelExpr = `openrouter(${JSON.stringify(backend.model)})`;
1152
+ setup = `\nconst openrouter = createOpenRouter({
1153
+ apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),${headersField}
1154
+ });\n`;
1155
+ const settings = reasoning
1156
+ ? `, { reasoning: { effort: ${JSON.stringify(reasoning)} } }`
1157
+ : "";
1158
+ modelExpr = `openrouter(${JSON.stringify(backend.model)}${settings})`;
1094
1159
  } else if (backend.kind === "openai-compatible") {
1095
1160
  imports.push(
1096
1161
  'import { createOpenAICompatible } from "@ai-sdk/openai-compatible";'
1097
1162
  );
1098
1163
  setup = `\nconst provider = createOpenAICompatible({
1099
1164
  apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),
1100
- baseURL: ${JSON.stringify(backend.baseUrl)},
1165
+ baseURL: ${JSON.stringify(backend.baseUrl)},${headersField}
1101
1166
  name: ${JSON.stringify(backend.name)},
1102
1167
  });\n`;
1103
1168
  modelExpr = `provider(${JSON.stringify(backend.model)})`;
@@ -1113,13 +1178,15 @@ export const askEndpointTemplate = (
1113
1178
  if (instructions) {
1114
1179
  groundFields.push(`instructions: ${JSON.stringify(instructions)}`);
1115
1180
  }
1116
- if (options?.retrieval) {
1117
- groundFields.push(`retrieval: ${JSON.stringify(options.retrieval)}`);
1181
+ if (retrieval) {
1182
+ groundFields.push(`retrieval: ${JSON.stringify(retrieval)}`);
1118
1183
  }
1119
1184
  const groundOptions =
1120
1185
  groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
1121
1186
  setup += `\nconst ground = createAskContext(askData${groundOptions});\n`;
1122
1187
  }
1188
+ const cors = askCorsTemplate(options?.cors);
1189
+ imports.push(...cors.imports);
1123
1190
  // Validate the client-supplied body and cap its size. The endpoint is
1124
1191
  // unauthenticated, so bounding message count/length limits how much a caller
1125
1192
  // can spend against the model per request, and restricting roles to
@@ -1176,24 +1243,29 @@ export const askEndpointTemplate = (
1176
1243
  const onError = ` onError({ error }) {
1177
1244
  console.error("Ask AI provider error:", error);
1178
1245
  },`;
1246
+ // The `streamText` argument list, built once so the grounded and plain
1247
+ // paths can't drift: they differ only in where the instructions come from.
1248
+ const streamFields = [
1249
+ `model: ${modelExpr}`,
1250
+ grounded
1251
+ ? "instructions"
1252
+ : `instructions:\n ${JSON.stringify(fallbackPrompt)}`,
1253
+ "messages",
1254
+ ];
1255
+ if (reasoning && backend.kind !== "openrouter") {
1256
+ streamFields.push(`reasoning: ${JSON.stringify(reasoning)}`);
1257
+ }
1258
+ const call = ` const result = streamText({
1259
+ ${streamFields.join(",\n ")},
1260
+ ${onError}
1261
+ });`;
1179
1262
  const stream = grounded
1180
1263
  ? ` const instructions =
1181
1264
  (await ground(messages, body.page)) ??
1182
1265
  ${JSON.stringify(fallbackPrompt)};
1183
- const result = streamText({
1184
- model: ${modelExpr},
1185
- instructions,
1186
- messages,
1187
- ${onError}
1188
- });`
1189
- : ` const result = streamText({
1190
- model: ${modelExpr},
1191
- instructions:
1192
- ${JSON.stringify(fallbackPrompt)},
1193
- messages,
1194
- ${onError}
1195
- });`;
1196
- const handler = `export const POST: APIRoute = async ({ request }) => {
1266
+ ${call}`
1267
+ : call;
1268
+ const handler = `export const POST: APIRoute = ${cors.open}
1197
1269
  ${validate}
1198
1270
  ${keyCheck}
1199
1271
  try {
@@ -1202,12 +1274,12 @@ ${stream}
1202
1274
  } catch {
1203
1275
  return new Response("Failed to generate a response.", { status: 500 });
1204
1276
  }
1205
- };`;
1277
+ ${cors.close}`;
1206
1278
  return `// Generated by Blume. Do not edit.
1207
1279
  ${imports.join("\n")}
1208
1280
 
1209
1281
  export const prerender = false;
1210
- ${setup}
1282
+ ${setup}${cors.setup}
1211
1283
  ${handler}
1212
1284
  `;
1213
1285
  };
@@ -1692,15 +1764,22 @@ export function GET({ props }: { props: { section: string } }) {
1692
1764
  * every collapsed section on every page. Ids are `navGroupIds` over the full
1693
1765
  * tree, the same ids the pages' sidebars use; the `version`/`locale`
1694
1766
  * segments pick the navigation variant (`current`/`default` for the
1695
- * unversioned, unlocalized tree). Rendered with no current route — a
1696
- * section that isn't on the active path has no active row by definition.
1767
+ * unversioned, unlocalized tree, and `default` again for the default locale
1768
+ * while its URL prefix is hidden: Astro's i18n routing 404s a page URL that
1769
+ * carries the default locale's code as a segment). Rendered with no current
1770
+ * route — a section that isn't on the active path has no active row by
1771
+ * definition.
1697
1772
  */
1698
1773
  export const navFragmentTemplate = (): string =>
1699
1774
  `---
1700
1775
  // Generated by Blume. Do not edit.
1701
1776
  import NavTree from "blume/components/layout/NavTree.astro";
1702
1777
  import { withBase } from "blume/components/islands/base-path.ts";
1703
- import { navGroupIds, navVariants } from "blume/components/layout/nav-utils.ts";
1778
+ import {
1779
+ hiddenDefaultLocale,
1780
+ navGroupIds,
1781
+ navVariants,
1782
+ } from "blume/components/layout/nav-utils.ts";
1704
1783
  import type { NavNode } from "blume/core/types.ts";
1705
1784
  import data from "blume:data";
1706
1785
 
@@ -1711,15 +1790,16 @@ export const partial = true;
1711
1790
  // Only imports are in scope here (Astro hoists getStaticPaths), which is why
1712
1791
  // the variant walk lives in nav-utils.
1713
1792
  export function getStaticPaths() {
1714
- return navVariants(data).flatMap(({ locale, navigation, version }) =>
1715
- [...navGroupIds(navigation.sidebar)]
1716
- .filter(([node]) => (node.display ?? "flat") !== "flat")
1717
- .map(([, id]) => ({ params: { id, locale, version } }))
1793
+ return navVariants(data, hiddenDefaultLocale(data.config.i18n)).flatMap(
1794
+ ({ locale, navigation, version }) =>
1795
+ [...navGroupIds(navigation.sidebar)]
1796
+ .filter(([node]) => (node.display ?? "flat") !== "flat")
1797
+ .map(([, id]) => ({ params: { id, locale, version } }))
1718
1798
  );
1719
1799
  }
1720
1800
 
1721
1801
  const { id, locale, version } = Astro.params;
1722
- const variant = navVariants(data).find(
1802
+ const variant = navVariants(data, hiddenDefaultLocale(data.config.i18n)).find(
1723
1803
  (entry) => entry.version === version && entry.locale === locale
1724
1804
  );
1725
1805
  const ids = variant
@@ -1933,37 +2013,22 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
1933
2013
  </ReferenceLayout>
1934
2014
  `;
1935
2015
 
1936
- export const catchAllPageTemplate = (options: {
1937
- exportEpub: boolean;
1938
- exportPdf: boolean;
1939
- mathEnabled: boolean;
1940
- /**
1941
- * Whether the sidebar defers collapsed sections to prerendered fragments
1942
- * (see `navFragmentTemplate`); true when some group renders as a
1943
- * disclosure or a drill-in panel.
1944
- */
1945
- navFragments?: boolean;
1946
- /** Serialize the island-hooks snapshot; only needed when React is enabled. */
1947
- needsReact: boolean;
1948
- }): string => {
1949
- const mathImport = options.mathEnabled
2016
+ /**
2017
+ * The component map every rendered entry body receives via
2018
+ * `<Content components={...} />`: Blume's content components, `<Math>` when
2019
+ * math is enabled, the island wrappers, and the user's `mdxComponents`
2020
+ * overrides. Shared by the catch-all page and the changelog index so a
2021
+ * `:::` directive (compiled to `<Callout>`) or an explicit `<Steps>` renders
2022
+ * wherever an entry is rendered — an MDX body throws "Expected component
2023
+ * `X` to be defined" the moment a page renders it without this map.
2024
+ */
2025
+ const contentComponentsSource = (mathEnabled: boolean) => {
2026
+ const mathImport = mathEnabled
1950
2027
  ? 'import Math from "blume/components/content/Math.astro";\n'
1951
2028
  : "";
1952
- const mathEntry = options.mathEnabled ? "Math,\n " : "";
1953
- // The island-hooks snapshot (config + navigation + page) for `blume/hooks`.
1954
- const clientData = options.needsReact
1955
- ? "\n clientData={{ config: data.config, navigation, page: { route, title: seo.title ?? title } }}"
1956
- : "";
1957
-
1958
- return `---
1959
- // Generated by Blume. Do not edit.
1960
- import { getEntry, render } from "astro:content";
1961
- import type { CollectionKey } from "astro:content";
1962
- import RootLayout from "blume/components/layout/RootLayout.astro";
1963
- import { withBase } from "blume/components/islands/base-path.ts";
1964
- import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
1965
- import { resolveSlot } from "blume/components/layout/overrides.ts";
1966
- import Accordion from "blume/components/content/Accordion.astro";
2029
+ const mathEntry = mathEnabled ? "Math,\n " : "";
2030
+ return {
2031
+ imports: `import Accordion from "blume/components/content/Accordion.astro";
1967
2032
  import AccordionItem from "blume/components/content/AccordionItem.astro";
1968
2033
  import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
1969
2034
  import Badge from "blume/components/content/Badge.astro";
@@ -2003,19 +2068,8 @@ import ApiOverview from "blume/components/openapi/ApiOverview.astro";
2003
2068
  import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
2004
2069
  import Operation from "blume/components/openapi/Operation.astro";
2005
2070
  ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
2006
- import { islandComponents } from "../generated/islands.ts";
2007
- import data from "blume:data";
2008
-
2009
- const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
2010
- const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
2011
-
2012
- // Docs content is file-based and always prerendered, even in server output
2013
- // (where only endpoints like /api/ask render on demand). Without this, server
2014
- // builds would render this route on demand and ignore getStaticPaths, leaving
2015
- // the entry id undefined.
2016
- export const prerender = true;
2017
-
2018
- const components = {
2071
+ import { islandComponents } from "../generated/islands.ts";`,
2072
+ map: `{
2019
2073
  Accordion,
2020
2074
  AccordionItem,
2021
2075
  ApiOverview,
@@ -2052,8 +2106,52 @@ const components = {
2052
2106
  YouTube,
2053
2107
  ${mathEntry}...islandComponents,
2054
2108
  ...userMdx,
2109
+ }`,
2110
+ };
2055
2111
  };
2056
2112
 
2113
+ export const catchAllPageTemplate = (options: {
2114
+ exportEpub: boolean;
2115
+ exportPdf: boolean;
2116
+ mathEnabled: boolean;
2117
+ /**
2118
+ * Whether the sidebar defers collapsed sections to prerendered fragments
2119
+ * (see `navFragmentTemplate`); true when some group renders as a
2120
+ * disclosure or a drill-in panel.
2121
+ */
2122
+ navFragments?: boolean;
2123
+ /** Serialize the island-hooks snapshot; only needed when React is enabled. */
2124
+ needsReact: boolean;
2125
+ }): string => {
2126
+ const { imports: componentImports, map: componentMap } =
2127
+ contentComponentsSource(options.mathEnabled);
2128
+ // The island-hooks snapshot (config + navigation + page) for `blume/hooks`.
2129
+ const clientData = options.needsReact
2130
+ ? "\n clientData={{ config: data.config, navigation, page: { route, title: seo.title ?? title } }}"
2131
+ : "";
2132
+
2133
+ return `---
2134
+ // Generated by Blume. Do not edit.
2135
+ import { getEntry, render } from "astro:content";
2136
+ import type { CollectionKey } from "astro:content";
2137
+ import RootLayout from "blume/components/layout/RootLayout.astro";
2138
+ import { withBase } from "blume/components/islands/base-path.ts";
2139
+ import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
2140
+ import { resolveSlot } from "blume/components/layout/overrides.ts";
2141
+ ${componentImports}
2142
+ import data from "blume:data";
2143
+
2144
+ const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
2145
+ const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
2146
+
2147
+ // Docs content is file-based and always prerendered, even in server output
2148
+ // (where only endpoints like /api/ask render on demand). Without this, server
2149
+ // builds would render this route on demand and ignore getStaticPaths, leaving
2150
+ // the entry id undefined.
2151
+ export const prerender = true;
2152
+
2153
+ const components = ${componentMap};
2154
+
2057
2155
  export function getStaticPaths() {
2058
2156
  return data.routes.map((route) => ({
2059
2157
  params: { slug: route.path === "/" ? undefined : route.path.slice(1) },
@@ -2324,7 +2422,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2324
2422
  exportEpub={${options.exportEpub}}${
2325
2423
  options.navFragments
2326
2424
  ? `
2327
- navFragmentBase={withBase(\`/blume-nav/\${version || "current"}/\${i18n ? locale : "default"}\`)}`
2425
+ navFragmentBase={withBase(\`/blume-nav/\${version || "current"}/\${i18n && localePrefix(locale) ? locale : "default"}\`)}`
2328
2426
  : ""
2329
2427
  }
2330
2428
  openInChat={data.config.openInChat}
@@ -2355,6 +2453,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2355
2453
  export const changelogIndexTemplate = (options: {
2356
2454
  exportEpub: boolean;
2357
2455
  exportPdf: boolean;
2456
+ mathEnabled: boolean;
2358
2457
  /** Serialize the island-hooks snapshot; only needed when React is enabled. */
2359
2458
  needsReact: boolean;
2360
2459
  /** Whether a `staged` collection exists (non-filesystem changelog sources). */
@@ -2368,6 +2467,8 @@ export const changelogIndexTemplate = (options: {
2368
2467
  const stagedSpread = options.staged
2369
2468
  ? '\n ...(await getCollection("staged")),'
2370
2469
  : "";
2470
+ const { imports: componentImports, map: componentMap } =
2471
+ contentComponentsSource(options.mathEnabled);
2371
2472
 
2372
2473
  return `---
2373
2474
  // Generated by Blume. Do not edit.
@@ -2377,11 +2478,19 @@ import Update from "blume/components/content/Update.astro";
2377
2478
  import { withBase } from "blume/components/islands/base-path.ts";
2378
2479
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2379
2480
  import { resolveDateFormatOptions } from "blume/core/date-format.ts";
2380
- import { layoutOverrides } from "../generated/components.ts";
2481
+ ${componentImports}
2381
2482
  import data from "blume:data";
2382
2483
 
2484
+ const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
2485
+ const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
2486
+
2383
2487
  export const prerender = true;
2384
2488
 
2489
+ // Entry bodies are MDX rendered outside the catch-all, so they need the same
2490
+ // component map: a \`:::\` callout or \`<Steps>\` in a release note otherwise
2491
+ // throws "Expected component ... to be defined" at build time.
2492
+ const components = ${componentMap};
2493
+
2385
2494
  const entryDate = (entry: {
2386
2495
  data: { date?: string | null; changelog?: { date?: string | null } | null };
2387
2496
  }) => entry.data.date ?? entry.data.changelog?.date ?? null;
@@ -2580,7 +2689,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2580
2689
  >
2581
2690
  {majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
2582
2691
  <Update description={date} href={href} id={id} label={label} tags={tags}>
2583
- <Content />
2692
+ <Content components={components} />
2584
2693
  </Update>
2585
2694
  ))}
2586
2695
  {majorGroups.slice(1).map((group) => (
@@ -2591,7 +2700,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2591
2700
  >
2592
2701
  {group.items.map(({ Content, href, id, label, date, tags }) => (
2593
2702
  <Update description={date} href={href} id={id} label={label} tags={tags}>
2594
- <Content />
2703
+ <Content components={components} />
2595
2704
  </Update>
2596
2705
  ))}
2597
2706
  </section>
@@ -2611,7 +2720,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2611
2720
  <div class="not-prose mt-8">
2612
2721
  {items.map(({ Content, href, id, label, date, tags }) => (
2613
2722
  <Update description={date} href={href} id={id} label={label} tags={tags}>
2614
- <Content />
2723
+ <Content components={components} />
2615
2724
  </Update>
2616
2725
  ))}
2617
2726
  </div>
@@ -20,7 +20,7 @@ const src = videoId ? youtubeEmbedUrl(videoId, { start }) : null;
20
20
 
21
21
  {
22
22
  src && (
23
- <div class="not-prose my-6 aspect-video overflow-hidden rounded-blume border border-border bg-muted/30">
23
+ <div class="not-prose my-6 aspect-video w-full overflow-hidden rounded-blume border border-border bg-muted/30">
24
24
  <iframe
25
25
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
26
26
  allowfullscreen
@@ -150,7 +150,7 @@ const ctaClass = [
150
150
  // so the offset can't drift while open, and re-measure when a banner dismiss or
151
151
  // a resize/rotation moves the header. Crossing into the desktop breakpoint
152
152
  // (64rem, Tailwind `lg`) closes the drawer so the scroll lock can't outlive it.
153
- const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-blume-header]");if(h){document.documentElement.style.setProperty("--blume-drawer-top",h.getBoundingClientRect().bottom+"px");}};document.addEventListener("click",(e)=>{const t=e.target.closest("[data-blume-theme-toggle]");if(t){const d=document.documentElement;const next=d.dataset.theme==="dark"?"light":"dark";const s=document.createElement("style");s.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important}"));document.head.appendChild(s);d.dataset.theme=next;localStorage.setItem("blume-theme",next);window.getComputedStyle(d).opacity;setTimeout(()=>document.head.removeChild(s),1);}const n=e.target.closest("[data-blume-nav-toggle]");if(n){const d=document.documentElement;const open=d.toggleAttribute("data-blume-nav-open");if(open){dr();}d.style.overflow=open?"hidden":"";}const b=e.target.closest("[data-blume-banner-dismiss]");if(b){const bar=b.closest("[data-blume-banner]");const k=bar&&bar.getAttribute("data-banner-key");if(k){localStorage.setItem("blume-banner:"+k,"1");}document.documentElement.setAttribute("data-blume-banner-hidden","");requestAnimationFrame(dr);}});addEventListener("resize",()=>{const d=document.documentElement;if(!d.hasAttribute("data-blume-nav-open")){return;}if(matchMedia("(min-width:64rem)").matches){d.removeAttribute("data-blume-nav-open");d.style.overflow="";}else{dr();}});})();`;
153
+ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-blume-header]");if(h){document.documentElement.style.setProperty("--blume-drawer-top",h.getBoundingClientRect().bottom+"px");}};document.addEventListener("click",(e)=>{const t=e.target.closest("[data-blume-theme-toggle]");if(t){const d=document.documentElement;const next=d.dataset.theme==="dark"?"light":"dark";const s=document.createElement("style");s.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important}"));document.head.appendChild(s);d.dataset.theme=next;localStorage.setItem("blume-theme",next);window.getComputedStyle(d).opacity;setTimeout(()=>document.head.removeChild(s),1);}const n=e.target.closest("[data-blume-nav-toggle]");if(n){const d=document.documentElement;const open=d.toggleAttribute("data-blume-nav-open");if(open){dr();}}const b=e.target.closest("[data-blume-banner-dismiss]");if(b){const bar=b.closest("[data-blume-banner]");const k=bar&&bar.getAttribute("data-banner-key");if(k){localStorage.setItem("blume-banner:"+k,"1");}document.documentElement.setAttribute("data-blume-banner-hidden","");requestAnimationFrame(dr);}});addEventListener("resize",()=>{const d=document.documentElement;if(!d.hasAttribute("data-blume-nav-open")){return;}if(matchMedia("(min-width:64rem)").matches){d.removeAttribute("data-blume-nav-open");}else{dr();}});})();`;
154
154
  ---
155
155
 
156
156
  <header