blume 1.6.5 → 1.7.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 (179) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0qhq7b8q.js +111 -0
  4. package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
  5. package/dist/cli/chunk-18tjv4f7.js +96 -0
  6. package/dist/cli/chunk-18tjv4f7.js.map +10 -0
  7. package/dist/cli/chunk-27gtm2ym.js +69 -0
  8. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  9. package/dist/cli/chunk-2aj8ddew.js +72 -0
  10. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  11. package/dist/cli/chunk-3r94j3tc.js +221 -0
  12. package/dist/cli/chunk-3r94j3tc.js.map +10 -0
  13. package/dist/cli/chunk-4trphnvy.js +102 -0
  14. package/dist/cli/chunk-4trphnvy.js.map +11 -0
  15. package/dist/cli/chunk-4xyggvgf.js +21 -0
  16. package/dist/cli/chunk-4xyggvgf.js.map +10 -0
  17. package/dist/cli/chunk-5d4q7121.js +4064 -0
  18. package/dist/cli/chunk-5d4q7121.js.map +40 -0
  19. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  20. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  21. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  22. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  23. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  24. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  25. package/dist/cli/chunk-9qs6acpw.js +176 -0
  26. package/dist/cli/chunk-9qs6acpw.js.map +10 -0
  27. package/dist/cli/chunk-agy5rzxy.js +2453 -0
  28. package/dist/cli/chunk-agy5rzxy.js.map +15 -0
  29. package/dist/cli/chunk-bcy492zc.js +16 -0
  30. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  31. package/dist/cli/chunk-btfr9yvw.js +41 -0
  32. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  33. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  34. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  35. package/dist/cli/chunk-cfw6x4rm.js +1967 -0
  36. package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
  37. package/dist/cli/chunk-ckh3a410.js +277 -0
  38. package/dist/cli/chunk-ckh3a410.js.map +11 -0
  39. package/dist/cli/chunk-drke6t0h.js +259 -0
  40. package/dist/cli/chunk-drke6t0h.js.map +11 -0
  41. package/dist/cli/chunk-ev67ycx0.js +15 -0
  42. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  43. package/dist/cli/chunk-ey89bjj1.js +209 -0
  44. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  45. package/dist/cli/chunk-j6pxe0dt.js +69 -0
  46. package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
  47. package/dist/cli/chunk-jk1zwka1.js +387 -0
  48. package/dist/cli/chunk-jk1zwka1.js.map +12 -0
  49. package/dist/cli/chunk-jtb45atp.js +467 -0
  50. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  51. package/dist/cli/chunk-jxkxjsc1.js +76 -0
  52. package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
  53. package/dist/cli/chunk-kwx90v78.js +81 -0
  54. package/dist/cli/chunk-kwx90v78.js.map +10 -0
  55. package/dist/cli/chunk-n0nyat6g.js +30 -0
  56. package/dist/cli/chunk-n0nyat6g.js.map +10 -0
  57. package/dist/cli/chunk-pxj10x8y.js +35 -0
  58. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  59. package/dist/cli/chunk-qq9nm3qd.js +1141 -0
  60. package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
  61. package/dist/cli/chunk-s102bysw.js +5170 -0
  62. package/dist/cli/chunk-s102bysw.js.map +47 -0
  63. package/dist/cli/chunk-s5dsk8bj.js +769 -0
  64. package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
  65. package/dist/cli/chunk-s5e5jt53.js +227 -0
  66. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  67. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  68. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  69. package/dist/cli/chunk-tnskyrej.js +117 -0
  70. package/dist/cli/chunk-tnskyrej.js.map +10 -0
  71. package/dist/cli/chunk-v2ymm99c.js +1016 -0
  72. package/dist/cli/chunk-v2ymm99c.js.map +13 -0
  73. package/dist/cli/chunk-v5mm027v.js +185 -0
  74. package/dist/cli/chunk-v5mm027v.js.map +11 -0
  75. package/dist/cli/chunk-vt8fgygt.js +23 -0
  76. package/dist/cli/chunk-vt8fgygt.js.map +10 -0
  77. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  78. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  79. package/dist/cli/chunk-wd27zjcz.js +60 -0
  80. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  81. package/dist/cli/chunk-x66c5yjn.js +23 -0
  82. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  83. package/dist/cli/chunk-xv91q4nm.js +5314 -0
  84. package/dist/cli/chunk-xv91q4nm.js.map +58 -0
  85. package/dist/cli/chunk-y3g15rvv.js +679 -0
  86. package/dist/cli/chunk-y3g15rvv.js.map +15 -0
  87. package/dist/cli/chunk-ye9zdkgv.js +136 -0
  88. package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
  89. package/dist/cli/chunk-ynacq3ev.js +1062 -0
  90. package/dist/cli/chunk-ynacq3ev.js.map +25 -0
  91. package/dist/cli/chunk-zr3ygrq3.js +54 -0
  92. package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
  93. package/dist/cli/index.js +55 -27597
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/components/layout/nav-utils.d.ts +33 -1
  97. package/dist/types/core/code-fences.d.ts +11 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +70 -0
  100. package/dist/types/theme/fonts.d.ts +22 -22
  101. package/docs/02-deployment.mdx +22 -1
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/navigation.mdx +2 -0
  105. package/docs/content/syntax.mdx +1 -1
  106. package/docs/discoverability/open-graph.mdx +4 -0
  107. package/docs/reference/cli.mdx +1 -1
  108. package/package.json +4 -2
  109. package/src/ai/api/handlers.ts +4 -7
  110. package/src/ai/api/paths.ts +8 -0
  111. package/src/ai/api/spec.ts +2 -1
  112. package/src/ai/ask-context.ts +378 -22
  113. package/src/astro/generate.ts +161 -28
  114. package/src/astro/include-hmr.ts +10 -13
  115. package/src/astro/include-refresh.ts +0 -0
  116. package/src/astro/index.ts +6 -1
  117. package/src/astro/integration.ts +280 -53
  118. package/src/astro/module-types.ts +83 -0
  119. package/src/astro/templates.ts +256 -108
  120. package/src/audit/image-size.ts +10 -8
  121. package/src/cli/command-meta.ts +77 -0
  122. package/src/cli/commands/add.ts +2 -4
  123. package/src/cli/commands/audit.ts +2 -4
  124. package/src/cli/commands/build.ts +70 -346
  125. package/src/cli/commands/check.ts +2 -4
  126. package/src/cli/commands/dev.ts +31 -42
  127. package/src/cli/commands/doctor.ts +2 -4
  128. package/src/cli/commands/eject.ts +3 -41
  129. package/src/cli/commands/eval.ts +2 -5
  130. package/src/cli/commands/init.ts +2 -4
  131. package/src/cli/commands/mcp-stdio.ts +2 -5
  132. package/src/cli/commands/preview.ts +3 -5
  133. package/src/cli/commands/sync.ts +2 -4
  134. package/src/cli/commands/translate.ts +2 -5
  135. package/src/cli/commands/validate.ts +2 -4
  136. package/src/cli/commands/version.ts +2 -4
  137. package/src/cli/eject-scripts.ts +0 -45
  138. package/src/cli/host-args.ts +16 -0
  139. package/src/cli/index.ts +84 -35
  140. package/src/cli/lazy-command.ts +47 -0
  141. package/src/components/Icon.astro +24 -0
  142. package/src/components/content/GithubInfo.astro +4 -1
  143. package/src/components/icon-sprite-middleware.ts +41 -0
  144. package/src/components/icon-sprite.ts +93 -0
  145. package/src/components/layout/IconSprite.astro +11 -0
  146. package/src/components/layout/NavTree.astro +156 -188
  147. package/src/components/layout/NavTreeCache.astro +45 -0
  148. package/src/components/layout/NavTreeScript.astro +256 -0
  149. package/src/components/layout/PageActions.astro +11 -5
  150. package/src/components/layout/PageLayout.astro +21 -3
  151. package/src/components/layout/ReferenceLayout.astro +21 -4
  152. package/src/components/layout/RootLayout.astro +44 -6
  153. package/src/components/layout/nav-cache.ts +49 -0
  154. package/src/components/layout/nav-utils.ts +69 -1
  155. package/src/components/layout/page-locale.ts +29 -0
  156. package/src/core/api-name.ts +18 -0
  157. package/src/core/code-fences.ts +48 -0
  158. package/src/core/content-assets.ts +3 -7
  159. package/src/core/includes.ts +3 -7
  160. package/src/core/package-root.ts +1 -1
  161. package/src/core/schema.ts +19 -0
  162. package/src/core/sources/normalize.ts +2 -37
  163. package/src/core/sources/obsidian.ts +3 -2
  164. package/src/core/svg-dimensions.ts +97 -0
  165. package/src/core/version-cut.ts +2 -2
  166. package/src/deploy/artifacts.ts +370 -0
  167. package/src/deploy/cloudflare-negotiation.ts +97 -32
  168. package/src/deploy/function-bundle.ts +66 -20
  169. package/src/deploy/sitemap.ts +6 -0
  170. package/src/deploy/vercel-negotiation.ts +8 -30
  171. package/src/markdown/language-icon.ts +64 -20
  172. package/src/markdown/mermaid.ts +11 -0
  173. package/src/og/cache.ts +236 -0
  174. package/src/og/card.ts +18 -16
  175. package/src/og/index.ts +8 -1
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +23 -10
  178. package/src/theme/entry.ts +41 -7
  179. package/src/theme/fonts.ts +30 -23
@@ -0,0 +1,2453 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ API_CATALOG_PATH,
4
+ OPENAPI_PATH,
5
+ hasApiCatalog
6
+ } from "./chunk-j6pxe0dt.js";
7
+ import {
8
+ searchProviderMeta
9
+ } from "./chunk-wd27zjcz.js";
10
+ import {
11
+ BLUME_IGNORE_DIRS,
12
+ TOC_HIDDEN_KEY
13
+ } from "./chunk-s102bysw.js";
14
+ import {
15
+ buildFontEntries,
16
+ fontLocaleCodes,
17
+ hasScalarReferences
18
+ } from "./chunk-xv91q4nm.js";
19
+ import {
20
+ normalizeBasePath,
21
+ trimChar,
22
+ withBasePath,
23
+ withComposedBasePath
24
+ } from "./chunk-27gtm2ym.js";
25
+
26
+ // src/deploy/adapter-output.ts
27
+ import { existsSync } from "node:fs";
28
+ import { cp, mkdir, rm } from "node:fs/promises";
29
+ import { dirname, join } from "pathe";
30
+ var ADAPTER_IGNORE_DIRS = {
31
+ netlify: ".netlify/",
32
+ vercel: ".vercel/"
33
+ };
34
+ var ADAPTER_OUTPUT_PATHS = {
35
+ netlify: ".netlify/v1"
36
+ };
37
+ var servesClientSubdir = (deployment) => deployment.output === "server" && (deployment.adapter === "node" || deployment.adapter === "cloudflare");
38
+ var deployStaticDir = (config, context) => {
39
+ const { adapter, output } = config.deployment;
40
+ if (output === "server" && adapter === "vercel") {
41
+ return join(context.root, ".vercel", "output", "static");
42
+ }
43
+ const dist = context.distDir ?? join(context.root, "dist");
44
+ if (servesClientSubdir(config.deployment)) {
45
+ return join(dist, "client");
46
+ }
47
+ return dist;
48
+ };
49
+ var surfaceAdapterOutput = async (config, context) => {
50
+ const { adapter, output } = config.deployment;
51
+ if (output !== "server" || !adapter) {
52
+ return { moved: false };
53
+ }
54
+ const rel = ADAPTER_OUTPUT_PATHS[adapter];
55
+ if (!rel) {
56
+ return { moved: false };
57
+ }
58
+ const from = join(context.outDir, rel);
59
+ const to = join(context.root, rel);
60
+ if (!existsSync(from)) {
61
+ return { moved: false };
62
+ }
63
+ await mkdir(dirname(to), { recursive: true });
64
+ await rm(to, { force: true, recursive: true });
65
+ await cp(from, to, { recursive: true, verbatimSymlinks: true });
66
+ await rm(from, { force: true, recursive: true });
67
+ return { from, moved: true, to };
68
+ };
69
+
70
+ // src/ai/link-headers.ts
71
+ var buildHomeLinkHeader = (config, routePaths) => {
72
+ const deployBase = normalizeBasePath(config.deployment.base);
73
+ const links = [];
74
+ if (hasApiCatalog(config)) {
75
+ links.push(`<${deployBase}${API_CATALOG_PATH}>; rel="api-catalog"; type="application/linkset+json"`);
76
+ }
77
+ if (config.ai.api) {
78
+ links.push(`<${deployBase}${OPENAPI_PATH}>; rel="service-desc"; type="application/json"`);
79
+ }
80
+ if (config.seo.agentReadability) {
81
+ links.push(`<${deployBase}/agent-readability.json>; rel="describedby"; type="application/json"`);
82
+ }
83
+ if (config.ai.llmsTxt.enabled) {
84
+ links.push(`<${deployBase}/llms.txt>; rel="describedby"; type="text/plain"`);
85
+ }
86
+ if (routePaths.includes("/")) {
87
+ links.push(`<${deployBase}/index.md>; rel="alternate"; type="text/markdown"`);
88
+ }
89
+ return links.length > 0 ? links.join(", ") : null;
90
+ };
91
+
92
+ // src/deploy/redirects.ts
93
+ var applyBaseToAstroRedirects = (redirects, basePath, deployBase) => {
94
+ const base = normalizeBasePath(deployBase);
95
+ return basePath || base ? redirects.map((redirect) => ({
96
+ ...redirect,
97
+ from: withBasePath(basePath, redirect.from),
98
+ to: withComposedBasePath(base, basePath, redirect.to)
99
+ })) : redirects;
100
+ };
101
+ var applyBaseToPlatformRedirects = (redirects, basePath, deployBase) => {
102
+ const base = normalizeBasePath(deployBase);
103
+ return basePath || base ? redirects.map((redirect) => ({
104
+ ...redirect,
105
+ from: withComposedBasePath(base, basePath, redirect.from),
106
+ to: withComposedBasePath(base, basePath, redirect.to)
107
+ })) : redirects;
108
+ };
109
+ var platformRedirects = (config) => applyBaseToPlatformRedirects(config.redirects, config.basePath, config.deployment.base ?? "");
110
+
111
+ // src/ai/ask.ts
112
+ var OPENAI_COMPATIBLE = "openai-compatible";
113
+ var OPENAI_COMPATIBLE_DEP = "@ai-sdk/openai-compatible";
114
+ var ASK_PRESETS = {
115
+ inkeep: {
116
+ apiKeyEnv: "INKEEP_API_KEY",
117
+ baseUrl: "https://api.inkeep.com/v1",
118
+ kind: OPENAI_COMPATIBLE,
119
+ name: "inkeep",
120
+ runtimeDep: OPENAI_COMPATIBLE_DEP
121
+ },
122
+ llmgateway: {
123
+ apiKeyEnv: "LLMGATEWAY_API_KEY",
124
+ baseUrl: "https://api.llmgateway.io/v1",
125
+ kind: OPENAI_COMPATIBLE,
126
+ name: "llmgateway",
127
+ runtimeDep: OPENAI_COMPATIBLE_DEP
128
+ },
129
+ openrouter: {
130
+ apiKeyEnv: "OPENROUTER_API_KEY",
131
+ kind: "openrouter",
132
+ name: "openrouter",
133
+ runtimeDep: "@openrouter/ai-sdk-provider"
134
+ }
135
+ };
136
+ var DEFAULT_MODEL = "openai/gpt-5.5";
137
+ var resolveAskBackend = (ask) => {
138
+ const provider = ask?.provider ?? "gateway";
139
+ const model = ask?.model ?? DEFAULT_MODEL;
140
+ if (provider === "gateway") {
141
+ return { kind: "gateway", model };
142
+ }
143
+ const preset = ASK_PRESETS[provider];
144
+ const apiKeyEnv = ask?.apiKeyEnv ?? preset?.apiKeyEnv ?? "API_KEY";
145
+ if (provider === "openrouter") {
146
+ return { apiKeyEnv, kind: "openrouter", model };
147
+ }
148
+ return {
149
+ apiKeyEnv,
150
+ baseUrl: ask?.baseUrl ?? preset?.baseUrl ?? "",
151
+ kind: OPENAI_COMPATIBLE,
152
+ model,
153
+ name: preset?.name ?? OPENAI_COMPATIBLE
154
+ };
155
+ };
156
+ var askBackendRuntimeDep = (ask) => {
157
+ const provider = ask?.provider ?? "gateway";
158
+ if (provider === "gateway") {
159
+ return;
160
+ }
161
+ return ASK_PRESETS[provider]?.runtimeDep ?? OPENAI_COMPATIBLE_DEP;
162
+ };
163
+
164
+ // src/astro/runtime-modules.ts
165
+ var RUNTIME_MODULE_FILES = new Map([
166
+ ["blume:ask-data", "ask-data.json"],
167
+ ["blume:content-assets", "content-assets.json"],
168
+ ["blume:data", "data.json"],
169
+ ["blume:mcp-data", "mcp-data.json"],
170
+ ["blume:openapi", "openapi.json"],
171
+ ["blume:raw-markdown", "raw-markdown.json"],
172
+ ["blume:rss", "rss.json"],
173
+ ["blume:search-index", "search.json"]
174
+ ]);
175
+ var RUNTIME_MODULE_IDS = new Set(RUNTIME_MODULE_FILES.keys());
176
+ var RESOLVED_PREFIX = "\x00";
177
+ var REGISTRY_KEY = Symbol.for("blume.runtime-modules");
178
+ var registry = () => {
179
+ const host = globalThis;
180
+ host[REGISTRY_KEY] ??= { modules: new Map, servers: new Set };
181
+ return host[REGISTRY_KEY];
182
+ };
183
+ var publishRuntimeModules = (modules) => {
184
+ const { modules: current, servers } = registry();
185
+ const changed = [];
186
+ for (const id of RUNTIME_MODULE_FILES.keys()) {
187
+ const next = modules.get(id);
188
+ if (current.get(id) === next) {
189
+ continue;
190
+ }
191
+ if (next === undefined) {
192
+ current.delete(id);
193
+ } else {
194
+ current.set(id, next);
195
+ }
196
+ changed.push(id);
197
+ }
198
+ if (changed.length === 0) {
199
+ return changed;
200
+ }
201
+ for (const server of servers) {
202
+ for (const id of changed) {
203
+ const mod = server.moduleGraph.getModuleById(`${RESOLVED_PREFIX}${id}`);
204
+ if (mod) {
205
+ server.moduleGraph.invalidateModule(mod);
206
+ }
207
+ }
208
+ server.ws.send({ type: "full-reload" });
209
+ }
210
+ return changed;
211
+ };
212
+
213
+ // src/astro/templates.ts
214
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
215
+ import { pathToFileURL } from "node:url";
216
+ import { dirname as dirname2, isAbsolute, join as join2, relative } from "pathe";
217
+ var WORKSPACE_MARKERS = [
218
+ ".git",
219
+ "bun.lock",
220
+ "bun.lockb",
221
+ "package-lock.json",
222
+ "pnpm-lock.yaml",
223
+ "pnpm-workspace.yaml",
224
+ "yarn.lock"
225
+ ];
226
+ var hasWorkspacesField = (pkgPath) => {
227
+ if (!existsSync2(pkgPath)) {
228
+ return false;
229
+ }
230
+ try {
231
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
232
+ return pkg.workspaces !== undefined;
233
+ } catch {
234
+ return false;
235
+ }
236
+ };
237
+ var hasWorkspaceMarker = (dir) => hasWorkspacesField(join2(dir, "package.json")) || WORKSPACE_MARKERS.some((marker) => existsSync2(join2(dir, marker)));
238
+ var findWorkspaceRoot = (start) => {
239
+ let dir = start;
240
+ for (;; ) {
241
+ if (hasWorkspaceMarker(dir)) {
242
+ return dir;
243
+ }
244
+ const parent = dirname2(dir);
245
+ if (parent === dir) {
246
+ return start;
247
+ }
248
+ dir = parent;
249
+ }
250
+ };
251
+ var ADAPTER_IMPORTS = {
252
+ cloudflare: "@astrojs/cloudflare",
253
+ netlify: "@astrojs/netlify",
254
+ node: "@astrojs/node",
255
+ vercel: "@astrojs/vercel"
256
+ };
257
+ var ADAPTER_OPTIONS = new Map([
258
+ ["node", '{ mode: "standalone" }']
259
+ ]);
260
+ var WRANGLER_CONFIG_FILES = [
261
+ "wrangler.jsonc",
262
+ "wrangler.json",
263
+ "wrangler.toml"
264
+ ];
265
+ var resolveCloudflareAdapterArgs = (context) => {
266
+ const args = [
267
+ 'prerenderEnvironment: "node"',
268
+ 'imageService: "compile"'
269
+ ];
270
+ const wranglerPath = WRANGLER_CONFIG_FILES.map((file) => join2(context.root, file)).find((file) => existsSync2(file));
271
+ if (wranglerPath) {
272
+ let configPath = relative(context.outDir, wranglerPath);
273
+ if (!configPath.startsWith(".") && !configPath.startsWith("/")) {
274
+ configPath = `./${configPath}`;
275
+ }
276
+ args.push(`configPath: ${JSON.stringify(configPath)}`);
277
+ }
278
+ return `{ ${args.join(", ")} }`;
279
+ };
280
+ var resolveSessionOption = (deployment) => deployment.output === "server" && deployment.adapter === "cloudflare" ? `
281
+ session: false,` : "";
282
+ var astroFontWeights = (weights) => JSON.stringify(weights).replaceAll(/(?<min>\d+)\.\.(?<max>\d+)/gu, "$<min> $<max>");
283
+ var astroConfigImportLine = (options) => {
284
+ const names = ["defineConfig"];
285
+ if (options.hasFonts) {
286
+ names.push("fontProviders");
287
+ }
288
+ return `import { ${names.join(", ")} } from "astro/config";`;
289
+ };
290
+ var runtimeDependencies = (options) => {
291
+ const { config, needsReact, needsSvelte, needsVue } = options;
292
+ const deps = ["@astrojs/mdx"];
293
+ if (needsReact) {
294
+ deps.push("@astrojs/react");
295
+ }
296
+ if (needsVue) {
297
+ deps.push("@astrojs/vue");
298
+ }
299
+ if (needsSvelte) {
300
+ deps.push("@astrojs/svelte");
301
+ }
302
+ if (hasScalarReferences(config)) {
303
+ deps.push("@scalar/astro");
304
+ }
305
+ deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
306
+ if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
307
+ const askDep = askBackendRuntimeDep(config.ai.ask);
308
+ if (askDep) {
309
+ deps.push(askDep);
310
+ }
311
+ }
312
+ const { deployment } = config;
313
+ if (deployment.output === "server" && deployment.adapter) {
314
+ const adapter = ADAPTER_IMPORTS[deployment.adapter];
315
+ if (adapter) {
316
+ deps.push(adapter);
317
+ }
318
+ }
319
+ return deps;
320
+ };
321
+ var RENDER_EXTERNAL_DEPS = [
322
+ "@astrojs/markdown-satteri",
323
+ "@pierre/diffs",
324
+ "@shikijs/transformers",
325
+ "@takumi-rs/core",
326
+ "@takumi-rs/helpers",
327
+ "@takumi-rs/wasm",
328
+ "github-slugger",
329
+ "katex",
330
+ "shiki",
331
+ "simple-icons",
332
+ "takumi-js",
333
+ "takumi-js/helpers",
334
+ "zod"
335
+ ];
336
+ var renderUserAliases = (aliases) => Object.entries(aliases ?? {}).toSorted(([a], [b]) => b.length - a.length).map(([find, replacement]) => `
337
+ ${JSON.stringify(find)}: ${JSON.stringify(replacement)},`).join("");
338
+ var astroOutDir = (context) => context.distDir ?? `${context.root}/dist`;
339
+ var adapterRoot = (context) => dirname2(astroOutDir(context));
340
+ var REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//, /\/\.cache\/vite\//]`;
341
+ var runtimeCacheOptions = (context, generatedModulesDir) => {
342
+ if (generatedModulesDir !== undefined) {
343
+ return { astro: "", vite: "" };
344
+ }
345
+ return {
346
+ astro: `
347
+ cacheDir: ${JSON.stringify(`${context.outDir}/.cache/astro`)},`,
348
+ vite: `
349
+ cacheDir: ${JSON.stringify(`${context.outDir}/.cache/vite`)},`
350
+ };
351
+ };
352
+ var reactIntegration = (compilerPath) => compilerPath ? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })` : `react({ ${REACT_EXCLUDE} })`;
353
+ var renderIntegrationBridge = (bridge) => {
354
+ if (!bridge) {
355
+ return {
356
+ configSourceMarker: "",
357
+ userConfigImports: "",
358
+ userConfigSetup: "",
359
+ userIntegrationSpread: ""
360
+ };
361
+ }
362
+ return {
363
+ configSourceMarker: bridge.sourceHash ? `// Blume config source SHA-256: ${bridge.sourceHash}
364
+ ` : "",
365
+ userConfigImports: `import { dirname, resolve } from "node:path";
366
+ import { fileURLToPath } from "node:url";
367
+ import { createModuleLoader } from "blume/core/load-module.ts";
368
+ `,
369
+ userConfigSetup: `const loadBlumeConfig = createModuleLoader();
370
+ const blumeConfig = await loadBlumeConfig(resolve(dirname(fileURLToPath(import.meta.url)), ${JSON.stringify(bridge.configFile)}));
371
+
372
+ `,
373
+ userIntegrationSpread: ", ...(blumeConfig?.integrations ?? [])"
374
+ };
375
+ };
376
+ var renderImageOption = (config) => config.image.domains.length > 0 || config.image.remotePatterns.length > 0 ? `
377
+ image: ${JSON.stringify(config.image)},` : "";
378
+ var ALL_CLIENT_FEATURES = { epub: true, mermaid: true };
379
+ var resolveOptimizeDeps = (options) => {
380
+ const { context, features } = options;
381
+ const optimizeDepsEntries = [
382
+ ...context.pagesRoot ? [`${context.pagesRoot}/**/*.astro`] : [],
383
+ `${context.root}/islands/**/*.{jsx,svelte,tsx,vue}`,
384
+ ...[...new Set(Object.values(options.aliases ?? {}))].toSorted().map((dir) => `${dir}/**/*.{astro,jsx,svelte,tsx,vue}`)
385
+ ];
386
+ const optimizeDepsInclude = [
387
+ ...features.mermaid ? ["blume > mermaid"] : [],
388
+ ...features.epub ? ["blume > epub-gen-memory/bundle"] : [],
389
+ ...options.needsReact && options.reactCompilerPath ? ["react/compiler-runtime"] : []
390
+ ];
391
+ return { optimizeDepsEntries, optimizeDepsInclude };
392
+ };
393
+ var renderRuntimeModuleWiring = (generatedModulesDir) => {
394
+ if (generatedModulesDir === undefined) {
395
+ return {
396
+ aliasLines: "",
397
+ imports: ["runtimeModulesPlugin"],
398
+ pluginEntry: "runtimeModulesPlugin(), "
399
+ };
400
+ }
401
+ const aliasLines = [...RUNTIME_MODULE_FILES].map(([id, file]) => `
402
+ ${JSON.stringify(id)}: ${JSON.stringify(`${generatedModulesDir}/${file}`)},`).join("");
403
+ return { aliasLines, imports: [], pluginEntry: "" };
404
+ };
405
+ var blumeIntegrationOptions = (options) => options.ejected ? {
406
+ buildArtifactsRoot: ".",
407
+ contentRoutes: options.contentRoutes,
408
+ homeLinkHeader: buildHomeLinkHeader(options.config, options.contentRoutes) ?? undefined,
409
+ pages: options.pages
410
+ } : { pages: options.pages };
411
+ var astroConfigTemplate = (options) => {
412
+ const { context, config, needsReact, pages, themePath } = options;
413
+ const { astro: cacheOptions, vite: viteCacheOption } = runtimeCacheOptions(context, options.generatedModulesDir);
414
+ const {
415
+ askPath,
416
+ contentRoutes,
417
+ examplesPath,
418
+ examplesThemePath,
419
+ features = ALL_CLIENT_FEATURES,
420
+ featuresPath,
421
+ generatedModulesDir,
422
+ needsSvelte,
423
+ needsVue,
424
+ searchClientPath
425
+ } = options;
426
+ const {
427
+ aliasLines: runtimeModuleAliasLines,
428
+ imports: runtimeModuleImports,
429
+ pluginEntry: runtimeModulesPluginEntry
430
+ } = renderRuntimeModuleWiring(generatedModulesDir);
431
+ const { deployment } = config;
432
+ const userAliasLines = renderUserAliases(options.aliases);
433
+ const server = deployment.output === "server";
434
+ const fsAllow = [...new Set([findWorkspaceRoot(context.root), context.root])];
435
+ const { optimizeDepsEntries, optimizeDepsInclude } = resolveOptimizeDeps({
436
+ aliases: options.aliases,
437
+ context,
438
+ features,
439
+ needsReact,
440
+ reactCompilerPath: options.reactCompilerPath
441
+ });
442
+ const adapterImport = server && deployment.adapter ? `import adapter from "${ADAPTER_IMPORTS[deployment.adapter]}";
443
+ ` : "";
444
+ const adapterArgs = (() => {
445
+ if (!server || !deployment.adapter) {
446
+ return "";
447
+ }
448
+ if (deployment.adapter === "cloudflare") {
449
+ return resolveCloudflareAdapterArgs(context);
450
+ }
451
+ return ADAPTER_OPTIONS.get(deployment.adapter) ?? "";
452
+ })();
453
+ const adapterExpr = deployment.adapter === "vercel" ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})` : `adapter(${adapterArgs})`;
454
+ const adapterOption = server && deployment.adapter ? `
455
+ adapter: ${adapterExpr},` : "";
456
+ const sessionOption = resolveSessionOption(deployment);
457
+ const siteOption = deployment.site ? `
458
+ site: ${JSON.stringify(deployment.site)},` : "";
459
+ const baseOption = deployment.base ? `
460
+ base: ${JSON.stringify(deployment.base)},` : "";
461
+ const imageOption = renderImageOption(config);
462
+ const i18nOption = config.i18n ? `
463
+ i18n: ${JSON.stringify({
464
+ defaultLocale: config.i18n.defaultLocale,
465
+ locales: config.i18n.locales.map((locale) => locale.code),
466
+ routing: {
467
+ prefixDefaultLocale: !config.i18n.hideDefaultLocalePrefix
468
+ }
469
+ })},` : "";
470
+ const basedRedirects = applyBaseToAstroRedirects(config.redirects, config.basePath, deployment.base ?? "");
471
+ const redirectsOption = basedRedirects.length > 0 ? `
472
+ redirects: ${JSON.stringify(Object.fromEntries(basedRedirects.map((redirect) => [
473
+ redirect.from,
474
+ { destination: redirect.to, status: redirect.status }
475
+ ])))},` : "";
476
+ const fontEntries = buildFontEntries(config.theme.fonts, fontLocaleCodes(config.i18n));
477
+ const fontsOption = fontEntries.length ? `
478
+ fonts: [${fontEntries.map((font) => font.kind === "local" ? `{ provider: fontProviders.local(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, fallbacks: ${JSON.stringify(font.fallbacks)}, options: { variants: ${JSON.stringify(font.variants.map((variant) => {
479
+ const face = {};
480
+ if (variant.weight !== undefined) {
481
+ face.weight = variant.weight;
482
+ }
483
+ if (variant.style !== undefined) {
484
+ face.style = variant.style;
485
+ }
486
+ return {
487
+ ...face,
488
+ src: [
489
+ isAbsolute(variant.src) ? variant.src : join2(context.root, variant.src)
490
+ ]
491
+ };
492
+ }))} } }` : `{ provider: fontProviders.${font.provider}(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, weights: ${astroFontWeights(font.weights)}, subsets: ${JSON.stringify(font.subsets)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`).join(", ")}],` : "";
493
+ const defineConfigImport = astroConfigImportLine({
494
+ hasFonts: fontEntries.length > 0
495
+ });
496
+ const reactImport = needsReact ? `import react from "@astrojs/react";
497
+ ` : "";
498
+ const vueImport = needsVue ? `import vue from "@astrojs/vue";
499
+ ` : "";
500
+ const svelteImport = needsSvelte ? `import svelte from "@astrojs/svelte";
501
+ ` : "";
502
+ const blumeImports = [
503
+ "blumeIntegration",
504
+ "includeHmrPlugin",
505
+ "prerenderDepsPlugin",
506
+ ...runtimeModuleImports,
507
+ ...adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []
508
+ ];
509
+ const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";
510
+ `;
511
+ const twoslashTransformer = "blumeTwoslashTransformer(), ";
512
+ const deployBase = normalizeBasePath(deployment.base);
513
+ const integrations = [
514
+ `mdx({ processor: blumeMdxProcessor(${JSON.stringify({
515
+ basePath: config.basePath,
516
+ codeThemes: config.markdown.codeBlocks.theme,
517
+ contentRoot: options.contentRoot,
518
+ deployBase,
519
+ headingAnchors: config.markdown.headingAnchors
520
+ })}) })`
521
+ ];
522
+ if (needsReact) {
523
+ integrations.push(reactIntegration(options.reactCompilerPath));
524
+ }
525
+ if (needsVue) {
526
+ integrations.push("vue()");
527
+ }
528
+ if (needsSvelte) {
529
+ integrations.push("svelte()");
530
+ }
531
+ integrations.push(`blumeIntegration(${JSON.stringify(blumeIntegrationOptions({
532
+ config,
533
+ contentRoutes,
534
+ ejected: generatedModulesDir !== undefined,
535
+ pages
536
+ }))})`);
537
+ const {
538
+ configSourceMarker,
539
+ userConfigImports,
540
+ userConfigSetup,
541
+ userIntegrationSpread
542
+ } = renderIntegrationBridge(options.integrationBridge);
543
+ return `// Generated by Blume. Do not edit; this file is recreated on each run.
544
+ ${configSourceMarker}${userConfigImports}import { availableParallelism } from "node:os";
545
+ ${defineConfigImport}
546
+ import mdx from "@astrojs/mdx";
547
+ import tailwindcss from "@tailwindcss/vite";
548
+ import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
549
+ ${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
550
+ ${userConfigSetup}export default defineConfig({
551
+ root: ${JSON.stringify(context.outDir)},
552
+ srcDir: ${JSON.stringify(`${context.outDir}/src`)},
553
+ outDir: ${JSON.stringify(astroOutDir(context))},
554
+ publicDir: ${JSON.stringify(`${context.root}/public`)},${cacheOptions}
555
+ output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${imageOption}${redirectsOption}${i18nOption}${fontsOption}
556
+ integrations: [${integrations.join(", ")}${userIntegrationSpread}],
557
+ markdown: {
558
+ processor: blumeMarkdownProcessor(${JSON.stringify({
559
+ basePath: config.basePath,
560
+ codeThemes: config.markdown.codeBlocks.theme,
561
+ contentRoot: options.contentRoot,
562
+ deployBase,
563
+ headingAnchors: config.markdown.headingAnchors
564
+ })}),
565
+ shikiConfig: {
566
+ themes: {
567
+ light: ${JSON.stringify(config.markdown.codeBlocks.theme.light)},
568
+ dark: ${JSON.stringify(config.markdown.codeBlocks.theme.dark)},
569
+ },
570
+ defaultColor: false,
571
+ transformers: [${twoslashTransformer}...blumeShikiTransformers(${JSON.stringify({ icons: config.markdown.code.icons })})],
572
+ },
573
+ },
574
+ devToolbar: { enabled: false },
575
+ // One canonical URL per page: canonicals, the sitemap, and hreflang all use
576
+ // the slashless form, so the slashed spelling is not a second address. Astro
577
+ // applies this itself — its dev server answers a slashed URL with a 404 that
578
+ // names the setting, an on-demand route redirects — and the Vercel adapter
579
+ // turns it into the platform's 308 route, so the Build Output config needs
580
+ // no hand-spliced redirect (see deploy/vercel-negotiation.ts). Static hosts
581
+ // serve the \`index.html\` directory layout as they always did.
582
+ trailingSlash: "never",
583
+ // The layouts render Astro's <ClientRouter />, and its in-place swaps read
584
+ // from the prefetch cache — fetching every link on hover/viewport hides the
585
+ // request latency behind the user's intent, so most navigations swap
586
+ // instantly.
587
+ prefetch: { prefetchAll: true },
588
+ // Prerender several pages at once. Rendering is single-threaded, but a
589
+ // page's OG card renders on a native thread and its HTML is written
590
+ // asynchronously, so the main thread would otherwise idle behind each
591
+ // page's off-thread work. Capped at 8: the gain flattens there and beyond it
592
+ // the overlap only adds memory.
593
+ build: { concurrency: Math.min(8, availableParallelism()) },
594
+ vite: {${viteCacheOption}
595
+ plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${JSON.stringify(`${context.outDir}/src/generated/includes.json`)}), prerenderDepsPlugin()],
596
+ // Everything hydration can reach must be part of the dev dep optimizer's
597
+ // FIRST run. The Vite root is the generated runtime, so user pages,
598
+ // islands, and aliased components live outside it and are only crawled
599
+ // when first requested — and \`react/compiler-runtime\` is Babel-injected,
600
+ // so no source scan can ever see it. A dependency discovered after
601
+ // hydration begins triggers a mid-session re-optimization whose new
602
+ // generation imports React through new \`?v=\` URLs; the browser then
603
+ // evaluates a second React copy and every island tears down with
604
+ // "Invalid hook call" (#157). \`entries\` points the startup scanner at
605
+ // the user's files (the scanner follows their imports, so their deps land
606
+ // in the initial optimization); the compiler runtime rides the include
607
+ // list because only the transform pipeline knows it exists.
608
+ //
609
+ // The mermaid/epub includes fix CJS interop instead: both lazy client-side
610
+ // imports land on CJS/UMD files (mermaid statically imports dayjs as CJS,
611
+ // epub-gen-memory's browser bundle is a browserified UMD) that break when
612
+ // served as raw ESM — mermaid throws on load and the EPUB export throws
613
+ // \`epub is not a function\`. They resolve through the \`blume\` package
614
+ // (they aren't direct deps of the generated project), so the nested
615
+ // \`blume > x\` form is required, and epub-gen-memory must name the
616
+ // \`/bundle\` subpath that is actually imported: optimizing the package
617
+ // root leaves that entry out. Production (Rollup) already handles the
618
+ // interop, so all of this only affects dev.
619
+ optimizeDeps: {
620
+ entries: ${JSON.stringify(optimizeDepsEntries)},
621
+ include: ${JSON.stringify(optimizeDepsInclude)},
622
+ },
623
+ // Blume's render-time deps are forced external on both build environments so
624
+ // native bindings resolve at runtime and isolated linkers don't bundle
625
+ // symlinked store copies (which would surface their children as unresolvable
626
+ // imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
627
+ //
628
+ // The SSR externals go through the legacy \`ssr.external\` key rather than
629
+ // \`environments.ssr\`: defining a user-owned \`environments.ssr\` block
630
+ // collides with the internal environment Astro 7 builds the server under and
631
+ // detaches the adapter's server entrypoint from the rolldown input, so the
632
+ // SSR entry is emitted as \`index.mjs\` instead of the \`entry.mjs\` the
633
+ // Vercel adapter's \`astro:build:done\` hook then fails to find. \`prerender\`
634
+ // is Astro-only and has no legacy equivalent, so it stays under \`environments\`.
635
+ ssr: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} },
636
+ environments: {
637
+ prerender: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
638
+ },
639
+ resolve: {
640
+ alias: {
641
+ "blume:ask": ${JSON.stringify(askPath)},
642
+ "blume:examples": ${JSON.stringify(examplesPath)},
643
+ "blume:examples-theme": ${JSON.stringify(examplesThemePath)},
644
+ "blume:features": ${JSON.stringify(featuresPath)},
645
+ "blume:search-client": ${JSON.stringify(searchClientPath)},
646
+ "blume:theme": ${JSON.stringify(themePath)},${runtimeModuleAliasLines}${userAliasLines}
647
+ },
648
+ },
649
+ server: {
650
+ fs: {
651
+ allow: ${JSON.stringify(fsAllow)},
652
+ },
653
+ },
654
+ },
655
+ });
656
+ `;
657
+ };
658
+ var stagedContentDir = (outDir) => join2(outDir, "content");
659
+ var runtimeDirWithin = (base, outDir) => {
660
+ const rel = relative(base, outDir);
661
+ return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : null;
662
+ };
663
+ var astroGlobBase = (base) => isAbsolute(base) ? pathToFileURL(base).href : base;
664
+ var contentConfigTemplate = (options) => {
665
+ const { context, config } = options;
666
+ const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
667
+ const collectionBase = options.collection?.base ?? context.contentRoot;
668
+ const includeGlobs = options.collection?.include ?? config.content.include;
669
+ const excludeGlobs = options.collection?.exclude ?? config.content.exclude;
670
+ const outDirRel = runtimeDirWithin(collectionBase, context.outDir);
671
+ const outDirIgnore = outDirRel ? [`!${outDirRel}/**`] : [];
672
+ const filesystem = options.filesystem ?? true;
673
+ const docsPattern = filesystem ? [
674
+ ...includeGlobs,
675
+ ...(excludeGlobs ?? []).map((pattern) => `!${pattern}`),
676
+ ...BLUME_IGNORE_DIRS.flatMap((dir) => dir === ".blume" ? [] : [`!**/${dir}/**`]),
677
+ ...outDirIgnore
678
+ ] : [];
679
+ const stagedBlock = options.staged ? `
680
+ const staged = defineCollection({
681
+ loader: glob({
682
+ pattern: ["**/*.{md,mdx}"],
683
+ base: ${JSON.stringify(astroGlobBase(stagedBase))},
684
+ generateId: ({ entry }) => entry,
685
+ }),
686
+ schema: pageCollectionSchema,
687
+ });
688
+ ` : "";
689
+ return `// Generated by Blume. Do not edit.
690
+ import { defineCollection } from "astro:content";
691
+ import { glob } from "astro/loaders";
692
+ import { withIncludeRefresh } from "blume/astro";
693
+ import { pageCollectionSchema } from "blume/core/schema.ts";
694
+
695
+ // withIncludeRefresh keeps <include>-bearing pages fresh: plain .md entries
696
+ // are rendered at sync time and digest-cached on the page file alone, so a
697
+ // partial edit (or a warm-cache rebuild after one) would serve stale HTML.
698
+ //
699
+ // pageCollectionSchema is the scan's page front-matter schema, so entry.data
700
+ // is typed and normalized the same way; it passes custom keys through and
701
+ // falls back to defaults for a page the scan already dropped as invalid.
702
+ const docs = defineCollection({
703
+ loader: withIncludeRefresh(glob({
704
+ pattern: ${JSON.stringify(docsPattern)},
705
+ base: ${JSON.stringify(astroGlobBase(collectionBase))},
706
+ generateId: ({ entry }) => entry,
707
+ }), ${JSON.stringify(`${context.outDir}/src/generated/includes.json`)}),
708
+ schema: pageCollectionSchema,
709
+ });
710
+ ${stagedBlock}
711
+ export const collections = { docs${options.staged ? ", staged" : ""} };
712
+ `;
713
+ };
714
+ var ASK_FALLBACK_PROMPT = "You are a helpful documentation assistant. Answer using the project's documentation.";
715
+ var askEndpointTemplate = (backend, grounded, options) => {
716
+ const instructions = options?.instructions;
717
+ const fallbackPrompt = instructions ? `${ASK_FALLBACK_PROMPT}
718
+
719
+ ${instructions}` : ASK_FALLBACK_PROMPT;
720
+ const imports = [
721
+ 'import type { APIRoute } from "astro";',
722
+ 'import { getSecret } from "astro:env/server";',
723
+ backend.kind === "gateway" ? 'import { createGateway, streamText } from "ai";' : 'import { streamText } from "ai";'
724
+ ];
725
+ let setup = "";
726
+ let modelExpr = JSON.stringify(backend.model);
727
+ if (backend.kind === "gateway") {
728
+ setup = `
729
+ const gateway = createGateway({ apiKey: getSecret("AI_GATEWAY_API_KEY") });
730
+ `;
731
+ modelExpr = `gateway(${JSON.stringify(backend.model)})`;
732
+ } else if (backend.kind === "openrouter") {
733
+ imports.push('import { createOpenRouter } from "@openrouter/ai-sdk-provider";');
734
+ setup = `
735
+ const openrouter = createOpenRouter({ apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}) });
736
+ `;
737
+ modelExpr = `openrouter(${JSON.stringify(backend.model)})`;
738
+ } else if (backend.kind === "openai-compatible") {
739
+ imports.push('import { createOpenAICompatible } from "@ai-sdk/openai-compatible";');
740
+ setup = `
741
+ const provider = createOpenAICompatible({
742
+ apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),
743
+ baseURL: ${JSON.stringify(backend.baseUrl)},
744
+ name: ${JSON.stringify(backend.name)},
745
+ });
746
+ `;
747
+ modelExpr = `provider(${JSON.stringify(backend.model)})`;
748
+ }
749
+ if (grounded) {
750
+ imports.push('import { createAskContext } from "blume/ai/ask-context.ts";', 'import askData from "blume:ask-data";');
751
+ const groundFields = [];
752
+ if (instructions) {
753
+ groundFields.push(`instructions: ${JSON.stringify(instructions)}`);
754
+ }
755
+ if (options?.retrieval) {
756
+ groundFields.push(`retrieval: ${JSON.stringify(options.retrieval)}`);
757
+ }
758
+ const groundOptions = groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
759
+ setup += `
760
+ const ground = createAskContext(askData${groundOptions});
761
+ `;
762
+ }
763
+ const validate = ` const body = await request.json().catch(() => null);
764
+ const raw = body?.messages;
765
+ const valid =
766
+ Array.isArray(raw) &&
767
+ raw.length > 0 &&
768
+ raw.length <= 40 &&
769
+ raw.every(
770
+ (m: unknown) =>
771
+ typeof m === "object" &&
772
+ m !== null &&
773
+ ("role" in m && (m.role === "user" || m.role === "assistant")) &&
774
+ ("content" in m && typeof m.content === "string")
775
+ ) &&
776
+ JSON.stringify(raw).length <= 24_000;
777
+ if (!valid) {
778
+ return new Response(
779
+ "Invalid request: send 1-40 user/assistant messages with string content.",
780
+ { status: 400 }
781
+ );
782
+ }
783
+ // Re-build the array so only role/content ever reach the model.
784
+ const messages = raw.map((m: { role: "user" | "assistant"; content: string }) => ({
785
+ content: m.content,
786
+ role: m.role,
787
+ }));`;
788
+ const keyCheck = backend.kind === "gateway" ? ` // The AI Gateway authenticates with an API key or Vercel's OIDC token.
789
+ if (!(getSecret("AI_GATEWAY_API_KEY") || getSecret("VERCEL_OIDC_TOKEN"))) {
790
+ return new Response(
791
+ "Ask AI is not configured: set AI_GATEWAY_API_KEY (or deploy on Vercel with OIDC).",
792
+ { status: 500 }
793
+ );
794
+ }` : ` if (!getSecret(${JSON.stringify(backend.apiKeyEnv)})) {
795
+ return new Response(
796
+ ${JSON.stringify(`Ask AI is not configured: set ${backend.apiKeyEnv}.`)},
797
+ { status: 500 }
798
+ );
799
+ }`;
800
+ const onError = ` onError({ error }) {
801
+ console.error("Ask AI provider error:", error);
802
+ },`;
803
+ const stream = grounded ? ` const instructions =
804
+ (await ground(messages, body.page)) ??
805
+ ${JSON.stringify(fallbackPrompt)};
806
+ const result = streamText({
807
+ model: ${modelExpr},
808
+ instructions,
809
+ messages,
810
+ ${onError}
811
+ });` : ` const result = streamText({
812
+ model: ${modelExpr},
813
+ instructions:
814
+ ${JSON.stringify(fallbackPrompt)},
815
+ messages,
816
+ ${onError}
817
+ });`;
818
+ const handler = `export const POST: APIRoute = async ({ request }) => {
819
+ ${validate}
820
+ ${keyCheck}
821
+ try {
822
+ ${stream}
823
+ return result.toTextStreamResponse();
824
+ } catch {
825
+ return new Response("Failed to generate a response.", { status: 500 });
826
+ }
827
+ };`;
828
+ return `// Generated by Blume. Do not edit.
829
+ ${imports.join(`
830
+ `)}
831
+
832
+ export const prerender = false;
833
+ ${setup}
834
+ ${handler}
835
+ `;
836
+ };
837
+ var askComponentTemplate = (askEnabled) => askEnabled ? `---
838
+ // Generated by Blume. Do not edit.
839
+ import AskAI from "blume/components/islands/AskAI.astro";
840
+ import data from "blume:data";
841
+
842
+ const { strings } = Astro.props;
843
+ ---
844
+
845
+ <AskAI
846
+ endpoint={data.config.ask?.endpoint ?? undefined}
847
+ strings={strings ?? data.ui.ask}
848
+ suggestions={data.config.ask?.suggestions ?? []}
849
+ />
850
+ ` : `---
851
+ // Generated by Blume. Do not edit.
852
+ // Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
853
+ // Deliberately imports no React island, keeping the JSX renderer out of projects
854
+ // that don't need it.
855
+ ---
856
+ `;
857
+ var searchEndpointTemplate = () => `// Generated by Blume. Do not edit.
858
+ import documents from "blume:search-index";
859
+
860
+ export const prerender = true;
861
+
862
+ export function GET() {
863
+ return new Response(JSON.stringify(documents), {
864
+ headers: { "Content-Type": "application/json" },
865
+ });
866
+ }
867
+ `;
868
+ var SEARCH_CLIENT_HEADER = `// Generated by Blume. Do not edit.
869
+ `;
870
+ var searchClientImport = (module) => `import { createSearch as create } from "blume/components/layout/search/${module}.ts";
871
+ `;
872
+ var SEARCH_BASE_IMPORT = `import { joinBase } from "blume/components/islands/base-path.ts";
873
+ `;
874
+ var staticSearchClient = (module, locale) => `${SEARCH_CLIENT_HEADER}${searchClientImport(module)}${SEARCH_BASE_IMPORT}
875
+ const indexUrl = joinBase(import.meta.env.BASE_URL, "blume-search.json");
876
+
877
+ export const createSearch = () => create({ indexUrl${locale ? `, locale: ${JSON.stringify(locale)}` : ""} });
878
+ `;
879
+ var hostedSearchClient = (module, options) => `${SEARCH_CLIENT_HEADER}${searchClientImport(module)}
880
+ export const createSearch = () => create(${JSON.stringify(options)});
881
+ `;
882
+ var hostedSearchOptions = (search) => {
883
+ switch (search.provider) {
884
+ case "algolia": {
885
+ return { module: "algolia", options: { ...search.algolia } };
886
+ }
887
+ case "orama-cloud": {
888
+ return {
889
+ module: "orama-cloud",
890
+ options: {
891
+ apiKey: search.oramaCloud?.apiKey,
892
+ endpoint: search.oramaCloud?.endpoint
893
+ }
894
+ };
895
+ }
896
+ case "typesense": {
897
+ return { module: "typesense", options: { ...search.typesense } };
898
+ }
899
+ default: {
900
+ return null;
901
+ }
902
+ }
903
+ };
904
+ var featuresTemplate = (features) => `// Generated by Blume. Do not edit.
905
+ //
906
+ // The client features this site uses. A feature no page needs stays out of
907
+ // the module graph entirely: its loader is null and its library never enters
908
+ // the client bundle.
909
+
910
+ /** Registers the <blume-mermaid> element; null when no page has a mermaid fence. */
911
+ export const loadMermaid: (() => Promise<unknown>) | null = ${features.mermaid ? '() => import("blume/components/content/mermaid-element.ts")' : "null"};
912
+
913
+ /** The EPUB generator's browser bundle; null when export.epub is off. */
914
+ export const loadEpub:
915
+ | (() => Promise<typeof import("epub-gen-memory/bundle")>)
916
+ | null = ${features.epub ? '() => import("epub-gen-memory/bundle")' : "null"};
917
+ `;
918
+ var searchClientTemplate = (config) => {
919
+ const { search } = config;
920
+ if (search.provider === "orama" || search.provider === "flexsearch") {
921
+ const locale = search.provider === "orama" ? config.i18n?.defaultLocale : undefined;
922
+ return staticSearchClient(search.provider, locale);
923
+ }
924
+ const hosted = hostedSearchOptions(search);
925
+ if (hosted) {
926
+ return hostedSearchClient(hosted.module, hosted.options);
927
+ }
928
+ if (search.provider === "mixedbread") {
929
+ return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}
930
+ const api = joinBase(import.meta.env.BASE_URL, "api/search");
931
+
932
+ export const createSearch = () => create({ api });
933
+ `;
934
+ }
935
+ if (search.provider === "pagefind") {
936
+ return `${SEARCH_CLIENT_HEADER}${searchClientImport("pagefind")}${SEARCH_BASE_IMPORT}
937
+ const url = joinBase(import.meta.env.BASE_URL, "pagefind/pagefind.js");
938
+
939
+ export const createSearch = () => create({ url });
940
+ `;
941
+ }
942
+ return `${SEARCH_CLIENT_HEADER}export const createSearch = () => () =>
943
+ Promise.resolve({ hits: [], sections: [] });
944
+ `;
945
+ };
946
+ var mixedbreadSearchEndpointTemplate = (storeId) => `// Generated by Blume. Do not edit.
947
+ import type { APIRoute } from "astro";
948
+ import { getSecret } from "astro:env/server";
949
+ import Mixedbread from "@mixedbread/sdk";
950
+
951
+ export const prerender = false;
952
+
953
+ const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" });
954
+ const STORE_ID = ${JSON.stringify(storeId)};
955
+
956
+ export const POST: APIRoute = async ({ request }) => {
957
+ // The endpoint is public: a malformed body must 200-empty, not 500.
958
+ const body = await request.json().catch(() => null);
959
+ const query = body?.query;
960
+ if (!query || typeof query !== "string") {
961
+ return new Response("[]", {
962
+ headers: { "Content-Type": "application/json" },
963
+ });
964
+ }
965
+ const response = await client.stores.search({
966
+ query,
967
+ store_identifiers: [STORE_ID],
968
+ top_k: 8,
969
+ });
970
+ const hits = (response.data ?? []).map((chunk) => {
971
+ const meta = chunk.generated_metadata ?? {};
972
+ return {
973
+ excerpt: chunk.text ?? meta.excerpt ?? "",
974
+ title: meta.title ?? chunk.filename ?? "",
975
+ url: meta.url ?? "",
976
+ };
977
+ });
978
+ return new Response(JSON.stringify(hits), {
979
+ headers: { "Content-Type": "application/json" },
980
+ });
981
+ };
982
+ `;
983
+ var rawMarkdownEndpointTemplate = (kind) => `// Generated by Blume. Do not edit.
984
+ import raw from "blume:raw-markdown";
985
+
986
+ export const prerender = true;
987
+
988
+ export function getStaticPaths() {
989
+ return Object.keys(raw).map((route) => ({
990
+ params: { slug: route === "/" ? "index" : route.slice(1) },
991
+ props: { route },
992
+ }));
993
+ }
994
+
995
+ export function GET({ props }: { props: { route: string } }) {
996
+ const entries = raw as Record<string, { md?: string; mdx?: string }>;
997
+ const entry = entries[props.route];
998
+ const body = entry ? ${kind === "md" ? '(entry.md ?? entry.mdx ?? "")' : '(entry.mdx ?? "")'} : "";
999
+ return new Response(body, {
1000
+ headers: {
1001
+ "Content-Type": "text/markdown; charset=utf-8",
1002
+ // ~4 characters per token; keep in sync with markdownTokenCount.
1003
+ "x-markdown-tokens": String(Math.ceil(body.length / 4)),
1004
+ },
1005
+ });
1006
+ }
1007
+ `;
1008
+ var contentAssetsEndpointTemplate = (stagedAssetsDir) => `// Generated by Blume. Do not edit.
1009
+ import { existsSync } from "node:fs";
1010
+ import { readdir, readFile } from "node:fs/promises";
1011
+ import { isAbsolute, join, relative, resolve } from "node:path";
1012
+ import type { APIRoute } from "astro";
1013
+ import assets from "blume:content-assets";
1014
+
1015
+ export const prerender = true;
1016
+
1017
+ const files = assets as Record<string, string>;
1018
+ const STAGED_DIR = ${JSON.stringify(stagedAssetsDir)};
1019
+
1020
+ const TYPES: Record<string, string> = {
1021
+ ".apng": "image/apng",
1022
+ ".avif": "image/avif",
1023
+ ".bmp": "image/bmp",
1024
+ ".gif": "image/gif",
1025
+ ".ico": "image/x-icon",
1026
+ ".jpeg": "image/jpeg",
1027
+ ".jpg": "image/jpeg",
1028
+ ".png": "image/png",
1029
+ ".svg": "image/svg+xml",
1030
+ ".tiff": "image/tiff",
1031
+ ".webp": "image/webp",
1032
+ };
1033
+
1034
+ const contentType = (path: string): string =>
1035
+ TYPES[path.slice(path.lastIndexOf(".")).toLowerCase()] ??
1036
+ "application/octet-stream";
1037
+
1038
+ const stagedParams = async (): Promise<string[]> => {
1039
+ if (!existsSync(STAGED_DIR)) {
1040
+ return [];
1041
+ }
1042
+ const entries = await readdir(STAGED_DIR, {
1043
+ recursive: true,
1044
+ withFileTypes: true,
1045
+ });
1046
+ return entries
1047
+ .filter((entry) => entry.isFile())
1048
+ .map((entry) =>
1049
+ relative(STAGED_DIR, join(entry.parentPath, entry.name)).replaceAll(
1050
+ "\\\\",
1051
+ "/"
1052
+ )
1053
+ );
1054
+ };
1055
+
1056
+ export async function getStaticPaths() {
1057
+ const params = [
1058
+ ...Object.keys(files).map((key) => "content/" + key),
1059
+ ...(await stagedParams()),
1060
+ ];
1061
+ return params.map((asset) => ({ params: { asset } }));
1062
+ }
1063
+
1064
+ const resolveAsset = (asset: string): string | null => {
1065
+ if (asset.startsWith("content/")) {
1066
+ return files[asset.slice("content/".length)] ?? null;
1067
+ }
1068
+ const abs = resolve(STAGED_DIR, asset);
1069
+ // Traversal guard: the dev server renders on demand, so the param is
1070
+ // attacker-controlled there — never step outside the staged directory.
1071
+ // path.relative rather than a string-prefix test: STAGED_DIR is baked in
1072
+ // with forward slashes while resolve() answers in the platform's
1073
+ // separators, so on Windows the prefix test 404'd every legitimate staged
1074
+ // asset — and a bare prefix also admits a sibling directory that merely
1075
+ // shares the name.
1076
+ const rel = relative(STAGED_DIR, abs);
1077
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
1078
+ return null;
1079
+ }
1080
+ return existsSync(abs) ? abs : null;
1081
+ };
1082
+
1083
+ export const GET: APIRoute = async ({ params }) => {
1084
+ const path = resolveAsset(String(params.asset ?? ""));
1085
+ if (!path) {
1086
+ return new Response(null, { status: 404 });
1087
+ }
1088
+ const body = await readFile(path);
1089
+ return new Response(new Uint8Array(body), {
1090
+ headers: { "Content-Type": contentType(path) },
1091
+ });
1092
+ };
1093
+ `;
1094
+ var mcpPageFile = (route) => `${trimChar(route, "/")}.ts`;
1095
+ var mcpEndpointTemplate = () => `// Generated by Blume. Do not edit.
1096
+ import type { APIRoute } from "astro";
1097
+ import { createMcpFetchHandler } from "blume/ai/mcp/server.ts";
1098
+ import data from "blume:mcp-data";
1099
+
1100
+ export const prerender = false;
1101
+
1102
+ const handler = createMcpFetchHandler(data);
1103
+
1104
+ export const ALL: APIRoute = ({ request }) => handler(request);
1105
+ `;
1106
+ var playgroundProxyTemplate = (origins) => `// Generated by Blume. Do not edit.
1107
+ import type { APIRoute } from "astro";
1108
+ import { createPlaygroundProxyHandler } from "blume/openapi/proxy.ts";
1109
+
1110
+ export const prerender = false;
1111
+
1112
+ const handler = createPlaygroundProxyHandler(${JSON.stringify(origins)});
1113
+
1114
+ export const ALL: APIRoute = ({ request }) => handler(request);
1115
+ `;
1116
+ var staticJsonEndpointTemplate = (payload) => `// Generated by Blume. Do not edit.
1117
+ export const prerender = true;
1118
+
1119
+ const payload = ${JSON.stringify(payload, null, 2)};
1120
+
1121
+ export function GET() {
1122
+ return new Response(JSON.stringify(payload), {
1123
+ headers: { "Content-Type": "application/json" },
1124
+ });
1125
+ }
1126
+ `;
1127
+ var rssEndpointTemplate = () => `// Generated by Blume. Do not edit.
1128
+ import feeds from "blume:rss";
1129
+
1130
+ export const prerender = true;
1131
+
1132
+ export function getStaticPaths() {
1133
+ return Object.keys(feeds).map((section) => ({
1134
+ params: { section },
1135
+ props: { section },
1136
+ }));
1137
+ }
1138
+
1139
+ export function GET({ props }: { props: { section: string } }) {
1140
+ const bySection = feeds as Record<string, string>;
1141
+ return new Response(bySection[props.section] ?? "", {
1142
+ headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
1143
+ });
1144
+ }
1145
+ `;
1146
+ var navFragmentTemplate = () => `---
1147
+ // Generated by Blume. Do not edit.
1148
+ import NavTree from "blume/components/layout/NavTree.astro";
1149
+ import { withBase } from "blume/components/islands/base-path.ts";
1150
+ import { navGroupIds, navVariants } from "blume/components/layout/nav-utils.ts";
1151
+ import type { NavNode } from "blume/core/types.ts";
1152
+ import data from "blume:data";
1153
+
1154
+ export const prerender = true;
1155
+ // A partial: no doctype, head, or script/style injection — the rows only.
1156
+ export const partial = true;
1157
+
1158
+ // Only imports are in scope here (Astro hoists getStaticPaths), which is why
1159
+ // the variant walk lives in nav-utils.
1160
+ export function getStaticPaths() {
1161
+ return navVariants(data).flatMap(({ locale, navigation, version }) =>
1162
+ [...navGroupIds(navigation.sidebar)]
1163
+ .filter(([node]) => (node.display ?? "flat") !== "flat")
1164
+ .map(([, id]) => ({ params: { id, locale, version } }))
1165
+ );
1166
+ }
1167
+
1168
+ const { id, locale, version } = Astro.params;
1169
+ const variant = navVariants(data).find(
1170
+ (entry) => entry.version === version && entry.locale === locale
1171
+ );
1172
+ const ids = variant
1173
+ ? navGroupIds(variant.navigation.sidebar)
1174
+ : new Map<NavNode, string>();
1175
+ const node = [...ids].find(([, groupId]) => groupId === id)?.[0];
1176
+ if (!(variant && node && node.kind === "group")) {
1177
+ return new Response(null, { status: 404 });
1178
+ }
1179
+ const strings =
1180
+ locale === "default" ? data.ui.nav : (data.uiByLocale[locale] ?? data.ui).nav;
1181
+ const fragmentBase = withBase(\`/blume-nav/\${version}/\${locale}\`);
1182
+ ---
1183
+
1184
+ <NavTree
1185
+ currentRoute=""
1186
+ depth={1}
1187
+ fragmentBase={fragmentBase}
1188
+ idPrefix={id}
1189
+ ids={ids}
1190
+ items={node.children}
1191
+ root={false}
1192
+ strings={strings}
1193
+ />
1194
+ `;
1195
+ var ogEndpointTemplate = (customRoutes = [], og = {}, includeChangelog = false) => `// Generated by Blume. Do not edit.
1196
+ import { cachedOgImage } from "blume/og";
1197
+ import type { OgCache, OgFont, OgFontFamilies } from "blume/og";
1198
+ import data from "blume:data";
1199
+
1200
+ export const prerender = true;
1201
+
1202
+ // Custom (non-content) pages opted into a generated card, baked in at build.
1203
+ // The annotation keeps the empty-array case from being an implicit any[]
1204
+ // (ts(7034)) under a strict tsconfig.
1205
+ const customRoutes: { slug: string; title: string }[] = ${JSON.stringify(customRoutes)};
1206
+
1207
+ // Card fonts, resolved at generation time (explicit seo.og.fonts, or derived
1208
+ // from theme.fonts). Local entries carry absolute build-machine paths, which
1209
+ // is why they are baked into this build-only endpoint instead of the runtime
1210
+ // data that pages serialize into HTML.
1211
+ const fonts: OgFont[] = ${JSON.stringify(og.fonts ?? [])};
1212
+ const families: OgFontFamilies | undefined = ${og.families ? JSON.stringify(og.families) : "undefined"};
1213
+
1214
+ // Rendered cards are kept on disk between builds, keyed by everything that
1215
+ // decides their pixels, so a rebuild renders only the cards whose title,
1216
+ // description, branding, or fonts changed. The directory is a build-machine
1217
+ // path, baked in for the same reason as the local font paths above.
1218
+ const cache: OgCache | undefined = ${og.cache ? JSON.stringify(og.cache) : "undefined"};
1219
+
1220
+ // A page's own description (its \`seo.description\`, else \`description\`) is
1221
+ // the card subtitle, so the image says what the page's og:description says.
1222
+ // Pages without one fall back to the site-wide subtitle at render time.
1223
+ // \`seo.og.description: false\` hides the subtitle on every card, page text
1224
+ // included, which is what switches this off.
1225
+ const pageDescriptions = ${og.pageDescriptions !== false};
1226
+
1227
+ interface CardProps {
1228
+ title: string;
1229
+ description: string | null;
1230
+ }
1231
+
1232
+ export function getStaticPaths() {
1233
+ const seen = new Set<string>();
1234
+ const paths: { params: { slug: string }; props: CardProps }[] = [];
1235
+ const add = (slug: string, title: string, description: string | null) => {
1236
+ if (seen.has(slug)) {
1237
+ return;
1238
+ }
1239
+ seen.add(slug);
1240
+ paths.push({
1241
+ params: { slug },
1242
+ props: { title, description: pageDescriptions ? description : null },
1243
+ });
1244
+ };
1245
+ // A custom page wins over a content route sharing its path, so add it first.
1246
+ // Its description is unknown at generate time, so it takes the site subtitle.
1247
+ for (const route of customRoutes) {
1248
+ add(route.slug, route.title, null);
1249
+ }
1250
+ for (const route of data.routes) {
1251
+ add(
1252
+ route.path === "/" ? "index" : route.path.slice(1),
1253
+ route.title,
1254
+ route.description
1255
+ );
1256
+ }${includeChangelog ? `
1257
+ // The generated changelog index is not a content route, so it needs its own
1258
+ // card. Added last: a custom page or content route owning /changelog wins.
1259
+ add(
1260
+ "changelog",
1261
+ data.ui.changelog?.title ?? "Changelog",
1262
+ data.ui.changelog?.description ?? null
1263
+ );` : ""}
1264
+ return paths;
1265
+ }
1266
+
1267
+ // Footer branding shared by every card. The slug comes from the configured
1268
+ // repo rather than the URL, so an Enterprise host reads the same as github.com;
1269
+ // the site text (host plus deployment base) is resolved at generate time.
1270
+ const repoSlug = data.config.github
1271
+ ? \`\${data.config.github.owner}/\${data.config.github.repo}\`
1272
+ : undefined;
1273
+
1274
+ export async function GET({ props }: { props: CardProps }) {
1275
+ const png = await cachedOgImage(cache, {
1276
+ accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
1277
+ brand: data.config.title,
1278
+ description: props.description ?? data.config.og.description,
1279
+ families,
1280
+ fonts,
1281
+ logo: data.config.og.logo,
1282
+ palette: data.config.og.palette,
1283
+ repo: repoSlug,
1284
+ site: data.config.og.site,
1285
+ title: props.title,
1286
+ });
1287
+ return new Response(new Uint8Array(png), {
1288
+ headers: {
1289
+ "Cache-Control": "public, max-age=31536000, immutable",
1290
+ "Content-Type": "image/png",
1291
+ },
1292
+ });
1293
+ }
1294
+ `;
1295
+ var scalarReferenceTemplate = (options) => `---
1296
+ // Generated by Blume. Do not edit.
1297
+ import { ScalarComponent } from "@scalar/astro";
1298
+ import ReferenceLayout from "blume/components/layout/ReferenceLayout.astro";
1299
+ import data from "blume:data";
1300
+
1301
+ export const prerender = true;
1302
+
1303
+ const configuration = ${JSON.stringify(options.configuration, null, 2)};
1304
+
1305
+ // The reference is an unlocalized route, so its chrome renders in the default
1306
+ // locale's language and direction (\`data.ui\` is the default locale's resolved
1307
+ // dictionary), mirroring the changelog index's locale wiring.
1308
+ const i18n = data.config.i18n;
1309
+ const localeMeta = i18n
1310
+ ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
1311
+ : null;
1312
+ const dir = localeMeta?.dir ?? "ltr";
1313
+ const htmlLang = i18n ? i18n.defaultLocale : "en";
1314
+ ---
1315
+
1316
+ <ReferenceLayout
1317
+ analytics={data.config.analytics}
1318
+ banner={data.config.banner}
1319
+ dir={dir}
1320
+ fontCssVars={data.fontCssVars}
1321
+ locale={htmlLang}
1322
+ logo={data.config.logo}
1323
+ favicon={data.config.favicon}
1324
+ appleIcon={data.config.appleIcon}
1325
+ navigation={data.navigation}
1326
+ noindex={${options.noindex === true}}
1327
+ pageTitle={${JSON.stringify(options.title)}}
1328
+ route={${JSON.stringify(options.route)}}
1329
+ searchEnabled={data.config.search.enabled}
1330
+ site={{ title: data.config.title, description: data.config.description }}
1331
+ themeMode={data.config.theme.mode}
1332
+ ui={data.ui}
1333
+ >
1334
+ <ScalarComponent configuration={configuration} renderMode="client" />
1335
+ </ReferenceLayout>
1336
+ `;
1337
+ var catchAllPageTemplate = (options) => {
1338
+ const mathImport = options.mathEnabled ? `import Math from "blume/components/content/Math.astro";
1339
+ ` : "";
1340
+ const mathEntry = options.mathEnabled ? `Math,
1341
+ ` : "";
1342
+ const clientData = options.needsReact ? `
1343
+ clientData={{ config: data.config, navigation, page: { route, title: seo.title ?? title } }}` : "";
1344
+ return `---
1345
+ // Generated by Blume. Do not edit.
1346
+ import { getEntry, render } from "astro:content";
1347
+ import type { CollectionKey } from "astro:content";
1348
+ import RootLayout from "blume/components/layout/RootLayout.astro";
1349
+ import { withBase } from "blume/components/islands/base-path.ts";
1350
+ import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
1351
+ import { resolveSlot } from "blume/components/layout/overrides.ts";
1352
+ import Accordion from "blume/components/content/Accordion.astro";
1353
+ import AccordionItem from "blume/components/content/AccordionItem.astro";
1354
+ import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
1355
+ import Badge from "blume/components/content/Badge.astro";
1356
+ import Callout from "blume/components/content/Callout.astro";
1357
+ import Card from "blume/components/content/Card.astro";
1358
+ import CardGroup from "blume/components/content/CardGroup.astro";
1359
+ import CodeBlock from "blume/components/content/CodeBlock.astro";
1360
+ import CodeGroup from "blume/components/content/CodeGroup.astro";
1361
+ import ColorRoot from "blume/components/content/Color.astro";
1362
+ import ColorItem from "blume/components/content/ColorItem.astro";
1363
+ import ColorRow from "blume/components/content/ColorRow.astro";
1364
+ import Column from "blume/components/content/Column.astro";
1365
+ import Columns from "blume/components/content/Columns.astro";
1366
+ import Component from "blume/components/content/Component.astro";
1367
+ import Diff from "blume/components/content/Diff.astro";
1368
+ import Expandable from "blume/components/content/Expandable.astro";
1369
+ import FileTree from "blume/components/content/FileTree.astro";
1370
+ import Frame from "blume/components/content/Frame.astro";
1371
+ import GithubInfo from "blume/components/content/GithubInfo.astro";
1372
+ import Panel from "blume/components/content/Panel.astro";
1373
+ import Prompt from "blume/components/content/Prompt.astro";
1374
+ import Step from "blume/components/content/Step.astro";
1375
+ import Steps from "blume/components/content/Steps.astro";
1376
+ import Tab from "blume/components/content/Tab.astro";
1377
+ import Tabs from "blume/components/content/Tabs.astro";
1378
+ import Tile from "blume/components/content/Tile.astro";
1379
+ import Tooltip from "blume/components/content/Tooltip.astro";
1380
+ import TreeRoot from "blume/components/content/Tree.astro";
1381
+ import TreeFile from "blume/components/content/TreeFile.astro";
1382
+ import TreeFolder from "blume/components/content/TreeFolder.astro";
1383
+ import TypeTable from "blume/components/content/TypeTable.astro";
1384
+ import Visibility from "blume/components/content/Visibility.astro";
1385
+ import YouTube from "blume/components/content/YouTube.astro";
1386
+ import Icon from "blume/components/Icon.astro";
1387
+ import LocaleLinks from "blume/components/layout/LocaleLinks.astro";
1388
+ import ApiOverview from "blume/components/openapi/ApiOverview.astro";
1389
+ import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
1390
+ import Operation from "blume/components/openapi/Operation.astro";
1391
+ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
1392
+ import { islandComponents } from "../generated/islands.ts";
1393
+ import data from "blume:data";
1394
+
1395
+ const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
1396
+ const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
1397
+
1398
+ // Docs content is file-based and always prerendered, even in server output
1399
+ // (where only endpoints like /api/ask render on demand). Without this, server
1400
+ // builds would render this route on demand and ignore getStaticPaths, leaving
1401
+ // the entry id undefined.
1402
+ export const prerender = true;
1403
+
1404
+ const components = {
1405
+ Accordion,
1406
+ AccordionItem,
1407
+ ApiOverview,
1408
+ ApiTagOperations,
1409
+ AutoTypeTable,
1410
+ Badge,
1411
+ Callout,
1412
+ Card,
1413
+ CardGroup,
1414
+ CodeBlock,
1415
+ CodeGroup,
1416
+ Color,
1417
+ Column,
1418
+ Columns,
1419
+ Component,
1420
+ Diff,
1421
+ Expandable,
1422
+ FileTree,
1423
+ Frame,
1424
+ GithubInfo,
1425
+ Icon,
1426
+ Operation,
1427
+ Panel,
1428
+ Prompt,
1429
+ Step,
1430
+ Steps,
1431
+ Tab,
1432
+ Tabs,
1433
+ Tile,
1434
+ Tooltip,
1435
+ Tree,
1436
+ TypeTable,
1437
+ Visibility,
1438
+ YouTube,
1439
+ ${mathEntry}...islandComponents,
1440
+ ...userMdx,
1441
+ };
1442
+
1443
+ export function getStaticPaths() {
1444
+ return data.routes.map((route) => ({
1445
+ params: { slug: route.path === "/" ? undefined : route.path.slice(1) },
1446
+ props: {
1447
+ alternates: route.alternates,
1448
+ collection: route.collection,
1449
+ editUrl: route.editUrl,
1450
+ entryId: route.entryId,
1451
+ fallback: route.fallback,
1452
+ indexable: route.indexable,
1453
+ lastModified: route.lastModified,
1454
+ locale: route.locale,
1455
+ route: route.path,
1456
+ title: route.title,
1457
+ version: route.version,
1458
+ versionAlternates: route.versionAlternates,
1459
+ },
1460
+ }));
1461
+ }
1462
+
1463
+ const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback, version, versionAlternates } = Astro.props;
1464
+ const entry = await getEntry(collection as CollectionKey, entryId);
1465
+ if (!entry) {
1466
+ return new Response(null, { status: 404 });
1467
+ }
1468
+ const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render(entry);
1469
+ // \`[!toc]\`-marked headings render on the page but stay out of the table of
1470
+ // contents; the heading plugin reports their slugs through the render's
1471
+ // frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
1472
+ // counts: \`frontmatter.extend\` can declare the same key, and on a page with
1473
+ // no headings that user-supplied value would pass straight through.
1474
+ const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
1475
+ const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
1476
+ const headings =
1477
+ tocHidden.size > 0
1478
+ ? allHeadings.filter((heading) => !tocHidden.has(heading.slug))
1479
+ : allHeadings;
1480
+ const frontmatter = entry.data;
1481
+
1482
+ const seo = frontmatter.seo;
1483
+ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
1484
+
1485
+ // Percent-encode the route-derived path (the sitemap convention): a Unicode
1486
+ // slug (\`/api/größe\`) is not legal in a raw URI, and crawlers compare
1487
+ // canonical against the sitemap's encoded <loc> byte-for-byte.
1488
+ const ogPath = data.config.og.enabled
1489
+ ? encodeURI(\`/og/\${route === "/" ? "index" : route.slice(1)}.png\`)
1490
+ : null;
1491
+ const ogRel = seo.image ?? ogPath;
1492
+ // Absolute URLs also carry the deployment base (the page is served under it):
1493
+ // \`site + base + path\`. Only absolutize root-relative paths: \`seo.image\` may be
1494
+ // an external URL, which passes through verbatim (mirrors PageLayout).
1495
+ const ogImage =
1496
+ ogRel && base && ogRel.startsWith("/") ? \`\${base}\${withBase(ogRel)}\` : ogRel;
1497
+ // Blume's generated card has known dimensions the layout can declare; a user's
1498
+ // \`seo.image\` could be any size or format, so it gets none.
1499
+ const ogGenerated = !seo.image && Boolean(ogPath);
1500
+
1501
+ // X attribution: the site's account, plus a creator the page can claim for
1502
+ // itself (a guest post crediting its own author) over the configured default.
1503
+ const x = { ...data.config.x, ...(seo.x?.creator ? { creator: seo.x.creator } : {}) };
1504
+
1505
+ const basedRoute = withBase(route);
1506
+
1507
+ // Locale resolution. With i18n on, pick the active locale's nav + dictionary,
1508
+ // build hreflang alternates, and derive the language-switcher targets.
1509
+ const i18n = data.config.i18n;
1510
+ const localePrefix = (codeArg: string) =>
1511
+ i18n && codeArg === i18n.defaultLocale && i18n.hideDefaultLocalePrefix
1512
+ ? ""
1513
+ : \`/\${codeArg}\`;
1514
+ const localizeRoute = (logical: string, codeArg: string) => {
1515
+ const prefix = localePrefix(codeArg);
1516
+ if (!prefix) {
1517
+ return logical;
1518
+ }
1519
+ return logical === "/" ? prefix : \`\${prefix}\${logical}\`;
1520
+ };
1521
+ const stripLocale = (path: string, codeArg: string) => {
1522
+ const prefix = localePrefix(codeArg);
1523
+ return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
1524
+ };
1525
+
1526
+ // Version resolution. An archived page renders its snapshot's navigation tree,
1527
+ // points its canonical at the latest equivalent (unless configured otherwise),
1528
+ // and shows the old-version notice.
1529
+ const versionsConfig = data.config.versions;
1530
+ const archived = versionsConfig && version
1531
+ ? (versionsConfig.archived.find((v) => v.id === version) ?? null)
1532
+ : null;
1533
+ const latestVersionAlt = (versionAlternates ?? []).find((alt) => alt.version === "");
1534
+
1535
+ const navigation = version
1536
+ ? (data.navigationByVersion[version]?.[i18n ? locale : ""] ?? data.navigation)
1537
+ : i18n
1538
+ ? (data.navigationByLocale[locale] ?? data.navigation)
1539
+ : data.navigation;
1540
+ const ui = i18n ? (data.uiByLocale[locale] ?? data.ui) : data.ui;
1541
+ const localeMeta = i18n ? i18n.locales.find((l) => l.code === locale) : null;
1542
+ const dir = localeMeta?.dir ?? "ltr";
1543
+ const htmlLang = i18n ? locale : "en";
1544
+ // A fallback page renders the fallback locale's content, so its text direction
1545
+ // follows that language — not the (mirrored) page locale.
1546
+ const contentLocale =
1547
+ fallback && i18n?.fallbackLocale ? i18n.fallbackLocale : locale;
1548
+ const contentDir = i18n
1549
+ ? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
1550
+ : "ltr";
1551
+ // The root route keeps its trailing slash (\`https://site/\`) so canonical and
1552
+ // hreflang URLs byte-match the sitemap's <loc> for the home page.
1553
+ const absolute = (path: string) => base + withBase(path);
1554
+
1555
+ // An archived page defaults its canonical to the same page in the latest docs
1556
+ // when that page still exists — search engines treat the live page as
1557
+ // authoritative without deindexing version-only content. A page's own
1558
+ // \`seo.canonical\` always wins, and \`canonical: "self"\` keeps the default.
1559
+ const canonical =
1560
+ seo.canonical ??
1561
+ (archived && archived.canonical === "latest" && latestVersionAlt && base
1562
+ ? absolute(latestVersionAlt.path)
1563
+ : base
1564
+ ? \`\${base}\${basedRoute === "/" ? "/" : encodeURI(basedRoute)}\`
1565
+ : null);
1566
+ const effectiveNoindex = Boolean(seo.noindex) || (archived?.noindex ?? false);
1567
+
1568
+ const localeAlternates =
1569
+ i18n && base
1570
+ ? (alternates ?? []).map((alt) => ({ hreflang: alt.locale, href: absolute(alt.path) }))
1571
+ : [];
1572
+ const defaultAlt = i18n ? (alternates ?? []).find((alt) => alt.locale === i18n.defaultLocale) : null;
1573
+ const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
1574
+
1575
+ // \`route\` arrives with \`basePath\` already applied, so the locale segment it
1576
+ // carries sits *after* the base (\`/docs/ja/guide\`), while \`localePrefix\` is
1577
+ // base-less (\`/ja\`). Stripping and re-adding a locale therefore happen in
1578
+ // base-less space, with the base re-applied at the end — the same
1579
+ // \`withBasePath(basePath, localizeRoute(...))\` composition the manifest uses to
1580
+ // build every real route. Done in based space, \`stripLocale\` matches nothing
1581
+ // and \`localizeRoute\` prepends a second prefix (\`/ja/docs/ja/guide\`). Only a
1582
+ // switcher entry for a locale with no real translation reaches this fallback:
1583
+ // a partially translated hand-written page for its missing locales, or a
1584
+ // generated reference (which has no \`alternates\` at all) for every locale
1585
+ // but its own.
1586
+ const mountLocalized = (logical: string, codeArg: string) =>
1587
+ withBasePath(data.config.basePath, localizeRoute(logical, codeArg));
1588
+ const logicalRoute = i18n
1589
+ ? stripLocale(stripBasePath(data.config.basePath, route), locale)
1590
+ : route;
1591
+ const localeSwitch = i18n
1592
+ ? i18n.locales.map((l) => {
1593
+ const alt = (alternates ?? []).find((x) => x.locale === l.code);
1594
+ return {
1595
+ code: l.code,
1596
+ current: l.code === locale,
1597
+ dir: l.dir,
1598
+ href: alt ? alt.path : mountLocalized(logicalRoute, l.code),
1599
+ label: l.label,
1600
+ untranslated: !alt,
1601
+ };
1602
+ })
1603
+ : [];
1604
+
1605
+ // Version switcher + old-version notice. The switcher auto-populates from the
1606
+ // versions config as a \`kind: "version"\` selector; a user-declared version
1607
+ // selector in \`navigation.selectors\` suppresses it (theirs renders instead).
1608
+ // Fallback version roots compose like real routes — \`{basePath}/{locale?}/{id}\`
1609
+ // (manifest \`versionAlternates\` paths arrive with the base already applied).
1610
+ const versionRootFor = (id: string) => {
1611
+ const logical = id ? \`/\${id}\` : "/";
1612
+ return i18n
1613
+ ? mountLocalized(logical, locale)
1614
+ : withBasePath(data.config.basePath, logical);
1615
+ };
1616
+ const samePageSwitch = versionsConfig
1617
+ ? versionsConfig.switcher.redirect === "same-page"
1618
+ : true;
1619
+ const userHasVersionSelector = navigation.selectors.some(
1620
+ (selector) => selector.kind === "version"
1621
+ );
1622
+ const versionSelector =
1623
+ versionsConfig && !userHasVersionSelector
1624
+ ? {
1625
+ items: [
1626
+ {
1627
+ id: "",
1628
+ label: versionsConfig.current.label,
1629
+ tag: versionsConfig.current.badge,
1630
+ },
1631
+ ...versionsConfig.archived.map((v) => ({
1632
+ id: v.id,
1633
+ label: v.label ?? v.id,
1634
+ tag: undefined,
1635
+ })),
1636
+ ].map((entry) => {
1637
+ const alt = (versionAlternates ?? []).find(
1638
+ (a) => a.version === entry.id
1639
+ );
1640
+ return {
1641
+ label: entry.label,
1642
+ path: samePageSwitch && alt ? alt.path : versionRootFor(entry.id),
1643
+ ...(entry.tag ? { tag: entry.tag } : {}),
1644
+ };
1645
+ }),
1646
+ kind: "version" as const,
1647
+ label: ui.versions.switcher,
1648
+ }
1649
+ : null;
1650
+
1651
+ const versionNotice =
1652
+ archived && archived.banner !== false
1653
+ ? {
1654
+ latestHref: latestVersionAlt
1655
+ ? latestVersionAlt.path
1656
+ : versionRootFor(""),
1657
+ latestLabel: ui.versions.latest,
1658
+ message:
1659
+ typeof archived.banner === "string"
1660
+ ? archived.banner
1661
+ : ui.versions.notice.replace(
1662
+ "{version}",
1663
+ archived.label ?? archived.id
1664
+ ),
1665
+ }
1666
+ : null;
1667
+
1668
+ // The whole page shell is overridable via \`layout.Layout\`; it receives the same
1669
+ // props as the built-in RootLayout, plus the \`layout\` map for its inner slots.
1670
+ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
1671
+ ---
1672
+
1673
+ <LayoutComponent
1674
+ site={{ title: data.config.title, description: data.config.description }}
1675
+ layout={layoutOverrides}${clientData}
1676
+ logo={data.config.logo}
1677
+ mcp={data.config.mcp}
1678
+ favicon={data.config.favicon}
1679
+ appleIcon={data.config.appleIcon}
1680
+ banner={data.config.banner}
1681
+ analytics={data.config.analytics}
1682
+ imageZoom={data.config.imageZoom}
1683
+ codeWrap={data.config.codeWrap}
1684
+ navigation={navigation}
1685
+ locale={htmlLang}
1686
+ dir={dir}
1687
+ contentDir={contentDir}
1688
+ ui={ui}
1689
+ localeAlternates={localeAlternates}
1690
+ xDefault={xDefault}
1691
+ localeSwitch={localeSwitch}
1692
+ versionSelector={versionSelector}
1693
+ versionNotice={versionNotice}
1694
+ searchVersion={versionsConfig ? version : null}
1695
+ page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
1696
+ headings={headings}
1697
+ toc={data.config.toc}
1698
+ dateFormat={data.config.dateFormat}
1699
+ themeMode={data.config.theme.mode}
1700
+ fontCssVars={data.fontCssVars}
1701
+ searchEnabled={data.config.search.enabled}
1702
+ indexable={indexable}
1703
+ ogImage={ogImage}
1704
+ ogGenerated={ogGenerated}
1705
+ x={x}
1706
+ canonical={canonical}
1707
+ editUrl={editUrl}
1708
+ feedback={data.config.feedback}
1709
+ exportPdf={${options.exportPdf}}
1710
+ exportEpub={${options.exportEpub}}${options.navFragments ? `
1711
+ navFragmentBase={withBase(\`/blume-nav/\${version || "current"}/\${i18n ? locale : "default"}\`)}` : ""}
1712
+ openInChat={data.config.openInChat}
1713
+ feeds={data.feeds}
1714
+ discovery={data.config.discovery}
1715
+ siteUrl={data.config.site}
1716
+ pageType={frontmatter.type}
1717
+ published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
1718
+ lastModified={lastModified}
1719
+ noindex={effectiveNoindex}
1720
+ structuredDataEnabled={data.config.structuredData}
1721
+ >
1722
+ <h1>{title}</h1>
1723
+ {frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
1724
+ <LocaleLinks locale={locale}>
1725
+ <Content components={components} />
1726
+ </LocaleLinks>
1727
+ </LayoutComponent>
1728
+ `;
1729
+ };
1730
+ var changelogIndexTemplate = (options) => {
1731
+ const clientData = options.needsReact ? `
1732
+ clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: pageTitle } }}` : "";
1733
+ const stagedSpread = options.staged ? `
1734
+ ...(await getCollection("staged")),` : "";
1735
+ return `---
1736
+ // Generated by Blume. Do not edit.
1737
+ import { getCollection, render } from "astro:content";
1738
+ import RootLayout from "blume/components/layout/RootLayout.astro";
1739
+ import Update from "blume/components/content/Update.astro";
1740
+ import { withBase } from "blume/components/islands/base-path.ts";
1741
+ import { resolveSlot } from "blume/components/layout/overrides.ts";
1742
+ import { resolveDateFormatOptions } from "blume/core/date-format.ts";
1743
+ import { layoutOverrides } from "../generated/components.ts";
1744
+ import data from "blume:data";
1745
+
1746
+ export const prerender = true;
1747
+
1748
+ const entryDate = (entry: {
1749
+ data: { date?: string | null; changelog?: { date?: string | null } | null };
1750
+ }) => entry.data.date ?? entry.data.changelog?.date ?? null;
1751
+
1752
+ const toTime = (value: string | null | undefined) => {
1753
+ if (!value) {
1754
+ return 0;
1755
+ }
1756
+ const date = new Date(value);
1757
+ return Number.isNaN(date.getTime()) ? 0 : date.getTime();
1758
+ };
1759
+
1760
+ // The changelog is an unlocalized route, so its chrome renders in the default
1761
+ // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
1762
+ // dictionary), mirroring the catch-all's locale wiring.
1763
+ const i18n = data.config.i18n;
1764
+ const localeMeta = i18n
1765
+ ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
1766
+ : null;
1767
+ const dir = localeMeta?.dir ?? "ltr";
1768
+ const htmlLang = i18n ? i18n.defaultLocale : "en";
1769
+
1770
+ // Formatted in the same locale as the chrome, and with the configured
1771
+ // \`dateFormat\` (UTC by default), to match the per-page "last updated" stamp.
1772
+ const dateFormatOptions = resolveDateFormatOptions(data.config.dateFormat);
1773
+ const formatDate = (value: string | null | undefined) => {
1774
+ if (!value) {
1775
+ return;
1776
+ }
1777
+ const date = new Date(value);
1778
+ return Number.isNaN(date.getTime())
1779
+ ? undefined
1780
+ : new Intl.DateTimeFormat(htmlLang, dateFormatOptions).format(date);
1781
+ };
1782
+
1783
+ const slugify = (text: string) =>
1784
+ text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
1785
+ "update";
1786
+
1787
+ // The major of a version's embedded semver (\`1.2.3\` -> 1, \`pkg@2.0.0\` -> 2), or
1788
+ // null when there is no full major.minor.patch to key on. Drives the changelog's
1789
+ // group-by-major pagination, so it tolerates the scoped tags monorepos publish.
1790
+ const majorVersion = (version: string | null | undefined) => {
1791
+ const match = /(\\d+)\\.\\d+\\.\\d+/.exec(String(version ?? ""));
1792
+ return match ? Number(match[1]) : null;
1793
+ };
1794
+
1795
+ // Map each entry to its own generated page so the timeline heading can deep-link
1796
+ // to it. The collection entry id matches the route manifest's \`entryId\`.
1797
+ const routeByEntry = new Map(
1798
+ data.routes.map((route) => [route.entryId, route.path])
1799
+ );
1800
+
1801
+ const changelogEntries = [
1802
+ ...(await getCollection("docs")),${stagedSpread}
1803
+ ]
1804
+ .filter(
1805
+ (entry) =>
1806
+ entry.data.type === "changelog" &&
1807
+ !entry.data.draft &&
1808
+ !entry.data.sidebar?.hidden
1809
+ )
1810
+ .toSorted((a, b) => toTime(entryDate(b)) - toTime(entryDate(a)));
1811
+
1812
+ const items = await Promise.all(
1813
+ changelogEntries.map(async (entry) => {
1814
+ const label =
1815
+ entry.data.title ??
1816
+ (entry.data.changelog?.version
1817
+ ? "v" + entry.data.changelog.version
1818
+ : "Update");
1819
+ return {
1820
+ Content: (await render(entry)).Content,
1821
+ date: formatDate(entryDate(entry)),
1822
+ href: routeByEntry.get(entry.id) ?? undefined,
1823
+ id: slugify(label),
1824
+ label,
1825
+ major: majorVersion(entry.data.changelog?.version),
1826
+ tags: entry.data.changelog?.category
1827
+ ? [entry.data.changelog.category]
1828
+ : [],
1829
+ };
1830
+ })
1831
+ );
1832
+
1833
+ // Repeated labels slug to the same id (e.g. two entries with neither a title
1834
+ // nor a version both falling back to "update"); suffix the later ones -2, -3,
1835
+ // ... so every heading deep-links to its own entry. The first keeps the plain
1836
+ // slug, and the rendered ids stay in lockstep with the \`headings\` list below.
1837
+ const seenIds = new Set();
1838
+ for (const item of items) {
1839
+ let uniqueId = item.id;
1840
+ for (let n = 2; seenIds.has(uniqueId); n += 1) {
1841
+ uniqueId = item.id + "-" + n;
1842
+ }
1843
+ seenIds.add(uniqueId);
1844
+ item.id = uniqueId;
1845
+ }
1846
+
1847
+ // A changelog is semver-paginated only when every visible release parses as
1848
+ // semver and they span more than one major line. Older majors then collapse
1849
+ // into groups the reader reveals one at a time; otherwise the timeline is flat.
1850
+ const majors = items.every((item) => item.major !== null)
1851
+ ? [...new Set(items.map((item) => item.major))]
1852
+ .filter((major): major is number => major !== null)
1853
+ .toSorted((a, b) => b - a)
1854
+ : [];
1855
+ const paginate = majors.length > 1;
1856
+ const majorGroups = majors.map((major) => ({
1857
+ items: items.filter((item) => item.major === major),
1858
+ label: major + ".x",
1859
+ major,
1860
+ }));
1861
+
1862
+ const headings = items.map((item) => ({
1863
+ depth: 2,
1864
+ slug: item.id,
1865
+ text: item.label,
1866
+ }));
1867
+
1868
+ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
1869
+ // The canonical URL carries the deployment base (the page is served under it),
1870
+ // matching how the catch-all canonicalizes via \`withBase(route)\`.
1871
+ const basedRoute = withBase("/changelog");
1872
+ const canonical = base ? base + basedRoute : null;
1873
+
1874
+ // The generated OG card for this route (the /og endpoint emits it alongside
1875
+ // the content-route cards), absolutized like the catch-all's so crawlers get
1876
+ // a full URL when the site is known.
1877
+ const ogPath = data.config.og.enabled ? withBase("/og/changelog.png") : null;
1878
+ const ogImage = ogPath && base ? base + ogPath : ogPath;
1879
+
1880
+ // The page chrome (h1, title, description) comes from the same translatable
1881
+ // \`changelog\` group as the reveal button; optional chaining tolerates a
1882
+ // not-yet-regenerated data snapshot from before these keys existed.
1883
+ const changelogTitle = data.ui.changelog?.title ?? "Changelog";
1884
+ const changelogDescription =
1885
+ data.ui.changelog?.description ??
1886
+ "Product updates, new features, and fixes from every release.";
1887
+ // The layout suffixes "- {site title}" itself, so the page title is just the
1888
+ // changelog's own name — prefixing the site title too would double it
1889
+ // ("Acme Changelog - Acme").
1890
+ const pageTitle = changelogTitle;
1891
+
1892
+ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
1893
+ ---
1894
+
1895
+ <LayoutComponent
1896
+ site={{ title: data.config.title, description: data.config.description }}
1897
+ layout={layoutOverrides}${clientData}
1898
+ logo={data.config.logo}
1899
+ mcp={data.config.mcp}
1900
+ favicon={data.config.favicon}
1901
+ appleIcon={data.config.appleIcon}
1902
+ banner={data.config.banner}
1903
+ analytics={data.config.analytics}
1904
+ imageZoom={data.config.imageZoom}
1905
+ codeWrap={data.config.codeWrap}
1906
+ navigation={data.navigation}
1907
+ locale={htmlLang}
1908
+ dir={dir}
1909
+ ui={data.ui}
1910
+ page={{
1911
+ title: pageTitle,
1912
+ description: changelogDescription,
1913
+ route: "/changelog",
1914
+ }}
1915
+ headings={headings}
1916
+ toc={data.config.toc}
1917
+ contentLayout="bare"
1918
+ themeMode={data.config.theme.mode}
1919
+ fontCssVars={data.fontCssVars}
1920
+ searchEnabled={data.config.search.enabled}
1921
+ indexable={true}
1922
+ ogImage={ogImage}
1923
+ ogGenerated={Boolean(ogImage)}
1924
+ x={data.config.x}
1925
+ canonical={canonical}
1926
+ exportPdf={${options.exportPdf}}
1927
+ exportEpub={${options.exportEpub}}
1928
+ openInChat={data.config.openInChat}
1929
+ feeds={data.feeds}
1930
+ discovery={data.config.discovery}
1931
+ siteUrl={data.config.site}
1932
+ noindex={false}
1933
+ structuredDataEnabled={data.config.structuredData}
1934
+ >
1935
+ <h1>{changelogTitle}</h1>
1936
+ {
1937
+ items.length === 0 ? (
1938
+ <p>No changelog entries yet.</p>
1939
+ ) : paginate ? (
1940
+ <blume-changelog
1941
+ class="not-prose mt-8 block"
1942
+ data-i18n-more={data.ui.changelog?.showReleases}
1943
+ >
1944
+ {majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
1945
+ <Update description={date} href={href} id={id} label={label} tags={tags}>
1946
+ <Content />
1947
+ </Update>
1948
+ ))}
1949
+ {majorGroups.slice(1).map((group) => (
1950
+ <section
1951
+ aria-label={group.label + " releases"}
1952
+ data-changelog-label={group.label}
1953
+ data-changelog-major={group.major}
1954
+ >
1955
+ {group.items.map(({ Content, href, id, label, date, tags }) => (
1956
+ <Update description={date} href={href} id={id} label={label} tags={tags}>
1957
+ <Content />
1958
+ </Update>
1959
+ ))}
1960
+ </section>
1961
+ ))}
1962
+ <div class="mt-10 flex justify-center">
1963
+ <button
1964
+ class="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground"
1965
+ data-changelog-more
1966
+ hidden
1967
+ type="button"
1968
+ >
1969
+ Show older releases
1970
+ </button>
1971
+ </div>
1972
+ </blume-changelog>
1973
+ ) : (
1974
+ <div class="not-prose mt-8">
1975
+ {items.map(({ Content, href, id, label, date, tags }) => (
1976
+ <Update description={date} href={href} id={id} label={label} tags={tags}>
1977
+ <Content />
1978
+ </Update>
1979
+ ))}
1980
+ </div>
1981
+ )
1982
+ }
1983
+ <script>
1984
+ import "blume/components/content/changelog-element.ts";
1985
+ </script>
1986
+ </LayoutComponent>
1987
+ `;
1988
+ };
1989
+ var notFoundPageTemplate = () => `---
1990
+ // Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
1991
+ import Icon from "blume/components/Icon.astro";
1992
+ import PageLayout from "blume/components/layout/PageLayout.astro";
1993
+ import { withBase } from "blume/components/islands/base-path.ts";
1994
+ import data from "blume:data";
1995
+
1996
+ export const prerender = true;
1997
+
1998
+ const nf = data.ui.notFound;
1999
+
2000
+ // The 404 page is an unlocalized route, so its chrome renders in the default
2001
+ // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
2002
+ // dictionary), mirroring the catch-all's locale wiring.
2003
+ const i18n = data.config.i18n;
2004
+ const localeMeta = i18n
2005
+ ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
2006
+ : null;
2007
+ const dir = localeMeta?.dir ?? "ltr";
2008
+ const htmlLang = i18n ? i18n.defaultLocale : "en";
2009
+
2010
+ // Recovery links, so a reader — or an agent that followed a stale URL — can
2011
+ // get back on track without guessing: every top-level section, then the
2012
+ // machine-readable indexes the build emits (the sitemap only exists with a
2013
+ // \`deployment.site\`; llms.txt only when \`ai.llmsTxt\` is on). Tabs link to
2014
+ // their resolved target when the section has no index page of its own.
2015
+ const suggestions = [
2016
+ ...data.navigation.tabs.map((tab) => ({
2017
+ href: withBase(tab.href ?? tab.path),
2018
+ label: tab.label,
2019
+ })),
2020
+ ...(data.config.discovery.sitemap
2021
+ ? [{ href: withBase("/sitemap.xml"), label: nf.sitemap }]
2022
+ : []),
2023
+ ...(data.config.discovery.llmsTxt
2024
+ ? [{ href: withBase("/llms.txt"), label: nf.llms }]
2025
+ : []),
2026
+ ];
2027
+ ---
2028
+
2029
+ <PageLayout
2030
+ site={{ title: data.config.title, description: data.config.description }}
2031
+ logo={data.config.logo}
2032
+ favicon={data.config.favicon}
2033
+ appleIcon={data.config.appleIcon}
2034
+ banner={data.config.banner}
2035
+ analytics={data.config.analytics}
2036
+ navigation={data.navigation}
2037
+ page={{ title: nf.title, route: "/404" }}
2038
+ themeMode={data.config.theme.mode}
2039
+ fontCssVars={data.fontCssVars}
2040
+ searchEnabled={data.config.search.enabled}
2041
+ locale={htmlLang}
2042
+ dir={dir}
2043
+ ui={data.ui}
2044
+ noindex={true}
2045
+ >
2046
+ <div
2047
+ class="mx-auto grid w-full max-w-5xl gap-12 px-6 py-20 sm:py-28 md:grid-cols-[3fr_2fr] md:gap-16 lg:gap-24 lg:py-36"
2048
+ >
2049
+ <div class="flex flex-col items-start">
2050
+ <p
2051
+ class="font-mono text-xs font-medium tracking-widest text-muted-foreground"
2052
+ >
2053
+ 404
2054
+ </p>
2055
+ <h1
2056
+ class="mt-4 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl"
2057
+ >
2058
+ {nf.title}
2059
+ </h1>
2060
+ <p class="mt-4 max-w-md text-pretty text-lg text-muted-foreground">
2061
+ {nf.description}
2062
+ </p>
2063
+ <a
2064
+ class="mt-8 inline-flex items-center gap-1.5 rounded-full bg-accent py-2 pe-4 ps-3.5 text-sm font-medium text-accent-foreground transition-opacity hover:opacity-90"
2065
+ href={withBase("/")}
2066
+ >
2067
+ <Icon class="rtl:-scale-x-100" name="arrow-left" size={14} />
2068
+ {nf.home}
2069
+ </a>
2070
+ </div>
2071
+ {
2072
+ suggestions.length > 0 && (
2073
+ <nav aria-label={nf.suggestions} class="md:border-s md:border-border md:ps-12 lg:ps-16">
2074
+ <h2 class="text-xs font-medium uppercase tracking-widest text-muted-foreground">
2075
+ {nf.suggestions}
2076
+ </h2>
2077
+ <ul class="mt-4 divide-y divide-border border-y border-border">
2078
+ {suggestions.map((link) => (
2079
+ <li>
2080
+ <a
2081
+ class="group flex items-center justify-between gap-4 py-3 text-sm font-medium text-foreground transition-colors hover:text-accent"
2082
+ href={link.href}
2083
+ >
2084
+ <span>{link.label}</span>
2085
+ <Icon
2086
+ class="shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 rtl:-scale-x-100 rtl:group-hover:-translate-x-0.5"
2087
+ name="arrow-right"
2088
+ size={14}
2089
+ />
2090
+ </a>
2091
+ </li>
2092
+ ))}
2093
+ </ul>
2094
+ </nav>
2095
+ )
2096
+ }
2097
+ </div>
2098
+ </PageLayout>
2099
+ `;
2100
+ var notFoundMarkdownTemplate = () => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
2101
+ import { withBase } from "blume/components/islands/base-path.ts";
2102
+ import { absoluteUrl } from "blume/core/site-url.ts";
2103
+ import data from "blume:data";
2104
+
2105
+ export const prerender = true;
2106
+
2107
+ const nf = data.ui.notFound;
2108
+
2109
+ // Absolute for internal routes when the site is known; an external tab href
2110
+ // passes through untouched.
2111
+ const href = (path: string): string => {
2112
+ const based = withBase(path);
2113
+ return data.config.site && based.startsWith("/") && !based.startsWith("//")
2114
+ ? absoluteUrl(data.config.site, based)
2115
+ : based;
2116
+ };
2117
+
2118
+ // The recovery set of 404.astro: home, every top-level section (a tab links to
2119
+ // its resolved target), then the machine-readable indexes that exist.
2120
+ const links = [
2121
+ { href: href("/"), label: nf.home },
2122
+ ...data.navigation.tabs.map((tab) => ({
2123
+ href: href(tab.href ?? tab.path),
2124
+ label: tab.label,
2125
+ })),
2126
+ ...(data.config.discovery.sitemap
2127
+ ? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
2128
+ : []),
2129
+ ...(data.config.discovery.llmsTxt
2130
+ ? [{ href: href("/llms.txt"), label: nf.llms }]
2131
+ : []),
2132
+ ...(data.config.discovery.api
2133
+ ? [{ href: href("/openapi.json"), label: nf.api }]
2134
+ : []),
2135
+ ];
2136
+
2137
+ const body = [
2138
+ "# " + nf.title,
2139
+ "",
2140
+ nf.description,
2141
+ "",
2142
+ "## " + nf.suggestions,
2143
+ "",
2144
+ ...links.map((link) => "- [" + link.label + "](" + link.href + ")"),
2145
+ "",
2146
+ ].join("\\n");
2147
+
2148
+ export function GET() {
2149
+ return new Response(body, {
2150
+ headers: {
2151
+ "Content-Type": "text/markdown; charset=utf-8",
2152
+ // ~4 characters per token; keep in sync with markdownTokenCount.
2153
+ "x-markdown-tokens": String(Math.ceil(body.length / 4)),
2154
+ },
2155
+ });
2156
+ }
2157
+ `;
2158
+ var notFoundJsonTemplate = () => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
2159
+ import { problem } from "blume/ai/api/problem.ts";
2160
+ import { withBase } from "blume/components/islands/base-path.ts";
2161
+ import { absoluteUrl } from "blume/core/site-url.ts";
2162
+ import data from "blume:data";
2163
+
2164
+ export const prerender = true;
2165
+
2166
+ const nf = data.ui.notFound;
2167
+
2168
+ // Absolute for internal routes when the site is known; an external tab href
2169
+ // passes through untouched.
2170
+ const href = (path: string): string => {
2171
+ const based = withBase(path);
2172
+ return data.config.site && based.startsWith("/") && !based.startsWith("//")
2173
+ ? absoluteUrl(data.config.site, based)
2174
+ : based;
2175
+ };
2176
+
2177
+ // The recovery set of 404.astro: home, every top-level section (a tab links to
2178
+ // its resolved target), then the machine-readable indexes that exist.
2179
+ const links = [
2180
+ { href: href("/"), label: nf.home },
2181
+ ...data.navigation.tabs.map((tab) => ({
2182
+ href: href(tab.href ?? tab.path),
2183
+ label: tab.label,
2184
+ })),
2185
+ ...(data.config.discovery.sitemap
2186
+ ? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
2187
+ : []),
2188
+ ...(data.config.discovery.llmsTxt
2189
+ ? [{ href: href("/llms.txt"), label: nf.llms }]
2190
+ : []),
2191
+ ...(data.config.discovery.api
2192
+ ? [{ href: href("/openapi.json"), label: nf.api }]
2193
+ : []),
2194
+ ];
2195
+
2196
+ const body = problem({
2197
+ code: "PAGE_NOT_FOUND",
2198
+ detail: nf.description,
2199
+ links,
2200
+ resolution: nf.suggestions + ": " + links.map((link) => link.href).join(", "),
2201
+ status: 404,
2202
+ title: nf.title,
2203
+ });
2204
+
2205
+ export function GET() {
2206
+ return new Response(JSON.stringify(body, null, 2) + "\\n", {
2207
+ headers: { "Content-Type": "application/problem+json; charset=utf-8" },
2208
+ });
2209
+ }
2210
+ `;
2211
+ var apiPagesIndexTemplate = () => `// Generated by Blume. Do not edit.
2212
+ import { pagesIndexResponse } from "blume/ai/api/handlers.ts";
2213
+ import data from "blume:mcp-data";
2214
+
2215
+ export const prerender = true;
2216
+
2217
+ export function GET() {
2218
+ return pagesIndexResponse(data);
2219
+ }
2220
+ `;
2221
+ var apiPageTemplate = () => `// Generated by Blume. Do not edit.
2222
+ import { pageParams, pageResponse } from "blume/ai/api/handlers.ts";
2223
+ import data from "blume:mcp-data";
2224
+
2225
+ export const prerender = true;
2226
+
2227
+ export function getStaticPaths() {
2228
+ return pageParams(data);
2229
+ }
2230
+
2231
+ export function GET({ props }: { props: { route: string } }) {
2232
+ return pageResponse(data, props.route);
2233
+ }
2234
+ `;
2235
+ var apiNavigationTemplate = () => `// Generated by Blume. Do not edit.
2236
+ import { navigationResponse } from "blume/ai/api/handlers.ts";
2237
+ import data from "blume:mcp-data";
2238
+
2239
+ export const prerender = true;
2240
+
2241
+ export function GET() {
2242
+ return navigationResponse(data);
2243
+ }
2244
+ `;
2245
+ var apiSearchTemplate = () => `// Generated by Blume. Do not edit.
2246
+ import type { APIRoute } from "astro";
2247
+ import { createSearchHandler } from "blume/ai/api/handlers.ts";
2248
+ import data from "blume:mcp-data";
2249
+
2250
+ export const prerender = false;
2251
+
2252
+ const handler = createSearchHandler(data);
2253
+
2254
+ export const GET: APIRoute = ({ request }) => handler(request);
2255
+ `;
2256
+ var apiNotFoundTemplate = (context) => `// Generated by Blume. Do not edit.
2257
+ import type { APIRoute } from "astro";
2258
+ import { apiNotFoundResponse } from "blume/ai/api/handlers.ts";
2259
+
2260
+ export const prerender = false;
2261
+
2262
+ const context = ${JSON.stringify(context)};
2263
+
2264
+ export const ALL: APIRoute = ({ request }) => apiNotFoundResponse(request, context);
2265
+ `;
2266
+ var islandDirective = (spec) => spec.client === "only" ? `client:only="${spec.framework}"` : `client:${spec.client}`;
2267
+ var wrapperPropsType = (name) => `type Props = typeof ${name} extends (
2268
+ props: infer P extends object,
2269
+ ...rest: never[]
2270
+ ) => unknown
2271
+ ? P
2272
+ : Record<string, unknown>;`;
2273
+ var islandWrapperTemplate = (spec) => `---
2274
+ // Generated by Blume. Do not edit.
2275
+ import Island from ${JSON.stringify(spec.file)};
2276
+ ${wrapperPropsType("Island")}
2277
+ ---
2278
+ <Island ${islandDirective(spec)} {...Astro.props}><slot /></Island>
2279
+ `;
2280
+ var islandMapTemplate = (specs) => {
2281
+ if (specs.length === 0) {
2282
+ return `// Generated by Blume. Do not edit.
2283
+ export const islandComponents = {};
2284
+ `;
2285
+ }
2286
+ const imports = specs.map((spec, index) => `import I${index} from "./islands/${spec.name}.astro";`).join(`
2287
+ `);
2288
+ const entries = specs.map((spec, index) => ` ${spec.name}: I${index},`).join(`
2289
+ `);
2290
+ return `// Generated by Blume. Do not edit.
2291
+ ${imports}
2292
+ export const islandComponents = {
2293
+ ${entries}
2294
+ };
2295
+ `;
2296
+ };
2297
+ var exampleDirective = (spec) => {
2298
+ if (spec.framework === "astro" || !spec.client) {
2299
+ return "";
2300
+ }
2301
+ return spec.client === "only" ? `client:only="${spec.framework}" ` : `client:${spec.client} `;
2302
+ };
2303
+ var exampleSlug = (path) => path.replaceAll(/[^a-zA-Z0-9]/gu, (char) => `_${(char.codePointAt(0) ?? 0).toString(16)}_`);
2304
+ var exampleWrapperTemplate = (spec) => `---
2305
+ // Generated by Blume. Do not edit.
2306
+ import Example from ${JSON.stringify(spec.file)};
2307
+ ${wrapperPropsType("Example")}
2308
+ ---
2309
+ <Example ${exampleDirective(spec)}{...Astro.props}><slot /></Example>
2310
+ `;
2311
+ var examplesRouteBase = (basePath) => `${basePath}/blume-examples`;
2312
+ var exampleMapTemplate = (specs, basePath) => {
2313
+ const base = `export const examplesBase = ${JSON.stringify(examplesRouteBase(basePath))};`;
2314
+ if (specs.length === 0) {
2315
+ return `// Generated by Blume. Do not edit.
2316
+ ${base}
2317
+ export const examples = {};
2318
+ `;
2319
+ }
2320
+ const imports = specs.map((spec, index) => `import E${index} from "./examples/${exampleSlug(spec.path)}.astro";`).join(`
2321
+ `);
2322
+ const entries = specs.map((spec, index) => ` ${JSON.stringify(spec.path)}: { Component: E${index}, code: ${JSON.stringify(spec.source)}, lang: ${JSON.stringify(spec.lang)} },`).join(`
2323
+ `);
2324
+ return `// Generated by Blume. Do not edit.
2325
+ ${imports}
2326
+ ${base}
2327
+ export const examples = {
2328
+ ${entries}
2329
+ };
2330
+ `;
2331
+ };
2332
+ var examplesPageTemplate = () => `---
2333
+ // Generated by Blume. Do not edit.
2334
+ import { examples } from "blume:examples";
2335
+ import "blume:examples-theme";
2336
+
2337
+ // Prerendered even in server output, like docs content.
2338
+ export const prerender = true;
2339
+
2340
+ export const getStaticPaths = () =>
2341
+ Object.keys(examples).map((path) => ({ params: { path } }));
2342
+
2343
+ const { path } = Astro.params;
2344
+ const entry = path ? examples[path] : undefined;
2345
+ if (!entry) {
2346
+ return new Response(null, { status: 404 });
2347
+ }
2348
+ const Example = entry.Component;
2349
+ ---
2350
+
2351
+ <html lang="en">
2352
+ <head>
2353
+ <meta charset="utf-8" />
2354
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2355
+ <meta name="robots" content="noindex" />
2356
+ <title>{path}</title>
2357
+ <script is:inline>
2358
+ (() => {
2359
+ const root = document.documentElement;
2360
+ const apply = (theme) => {
2361
+ root.dataset.theme = theme;
2362
+ root.classList.toggle("dark", theme === "dark");
2363
+ };
2364
+ const stored = () =>
2365
+ localStorage.getItem("blume-theme") ??
2366
+ (matchMedia("(prefers-color-scheme: dark)").matches
2367
+ ? "dark"
2368
+ : "light");
2369
+ try {
2370
+ const host = window.parent.document.documentElement;
2371
+ apply(host.dataset.theme ?? stored());
2372
+ new MutationObserver(() => {
2373
+ apply(host.dataset.theme ?? stored());
2374
+ }).observe(host, { attributeFilter: ["data-theme"] });
2375
+ } catch {
2376
+ apply(stored());
2377
+ }
2378
+ })();
2379
+ </script>
2380
+ </head>
2381
+ <!-- Flex + margin:auto centers the example and, unlike place-items, keeps
2382
+ the top edge reachable when the example outgrows the frame. -->
2383
+ <body style="display:flex;min-height:100svh;padding:1.5rem">
2384
+ <div data-blume-example style="margin:auto"><Example /></div>
2385
+ <script is:inline>
2386
+ (() => {
2387
+ // Report the example's rendered height so the embedding docs page can
2388
+ // size the preview pane to the content. The wrapper is observed rather
2389
+ // than the body: the body stretches to the frame's own height, so it
2390
+ // would only echo the pane back. Direct opens have no distinct parent
2391
+ // and skip out; the frame is same-origin with the docs page (see the
2392
+ // theme sync above), so the origin is pinned on both ends.
2393
+ if (window.parent === window) {
2394
+ return;
2395
+ }
2396
+ const wrapper = document.querySelector("[data-blume-example]");
2397
+ if (!wrapper) {
2398
+ return;
2399
+ }
2400
+ // The body's padding frames the example; fold it into the report so
2401
+ // the parent can apply the number as-is. Read from the live value —
2402
+ // the user's examples.css is injected after Blume's defaults precisely
2403
+ // so their tokens win, so a root font-size override must be honored
2404
+ // rather than assuming 1.5rem is 48px.
2405
+ const bodyStyle = getComputedStyle(document.body);
2406
+ const paddingPx =
2407
+ parseFloat(bodyStyle.paddingTop) + parseFloat(bodyStyle.paddingBottom);
2408
+ const report = () => {
2409
+ window.parent.postMessage(
2410
+ {
2411
+ height:
2412
+ Math.ceil(wrapper.getBoundingClientRect().height) + paddingPx,
2413
+ type: "blume:example-height",
2414
+ },
2415
+ window.location.origin
2416
+ );
2417
+ };
2418
+ new ResizeObserver(report).observe(wrapper);
2419
+ // The parent asks for a fresh report when its listener comes up, in
2420
+ // case the first one above was posted before anyone was listening.
2421
+ window.addEventListener("message", (event) => {
2422
+ if (
2423
+ event.source === window.parent &&
2424
+ event.origin === window.location.origin &&
2425
+ event.data?.type === "blume:example-height-request"
2426
+ ) {
2427
+ report();
2428
+ }
2429
+ });
2430
+ })();
2431
+ </script>
2432
+ </body>
2433
+ </html>
2434
+ `;
2435
+ var runtimePackageTemplate = (dependencies = []) => `${JSON.stringify({
2436
+ dependencies: Object.fromEntries([...dependencies].toSorted().map((name) => [name, "*"])),
2437
+ name: "blume-runtime",
2438
+ private: true,
2439
+ type: "module",
2440
+ version: "0.0.0"
2441
+ }, null, 2)}
2442
+ `;
2443
+ var runtimeTsconfigTemplate = () => `${JSON.stringify({
2444
+ exclude: ["dist"],
2445
+ extends: "astro/tsconfigs/strict",
2446
+ include: [".astro/types.d.ts", "**/*"]
2447
+ }, null, 2)}
2448
+ `;
2449
+
2450
+ export { ADAPTER_IGNORE_DIRS, servesClientSubdir, deployStaticDir, surfaceAdapterOutput, resolveAskBackend, askBackendRuntimeDep, buildHomeLinkHeader, platformRedirects, publishRuntimeModules, runtimeDependencies, astroConfigTemplate, stagedContentDir, contentConfigTemplate, askEndpointTemplate, askComponentTemplate, searchEndpointTemplate, featuresTemplate, searchClientTemplate, mixedbreadSearchEndpointTemplate, rawMarkdownEndpointTemplate, contentAssetsEndpointTemplate, mcpPageFile, mcpEndpointTemplate, playgroundProxyTemplate, staticJsonEndpointTemplate, rssEndpointTemplate, navFragmentTemplate, ogEndpointTemplate, scalarReferenceTemplate, catchAllPageTemplate, changelogIndexTemplate, notFoundPageTemplate, notFoundMarkdownTemplate, notFoundJsonTemplate, apiPagesIndexTemplate, apiPageTemplate, apiNavigationTemplate, apiSearchTemplate, apiNotFoundTemplate, islandWrapperTemplate, islandMapTemplate, exampleSlug, exampleWrapperTemplate, examplesRouteBase, exampleMapTemplate, examplesPageTemplate, runtimePackageTemplate, runtimeTsconfigTemplate };
2451
+
2452
+ //# debugId=539E38867287181364756E2164756E21
2453
+ //# sourceMappingURL=chunk-agy5rzxy.js.map