blume 1.0.2 → 1.0.3

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.
@@ -446,7 +446,21 @@ export interface LlmsTxtConfig {
446
446
  */
447
447
  openapi?: boolean;
448
448
  }
449
- /** AI-facing features: the Ask AI assistant and an `llms.txt` manifest. */
449
+ /** Expose the docs as an MCP server for connecting agents. */
450
+ export interface McpConfig {
451
+ /** Turn the MCP server on. Defaults to `false`. */
452
+ enabled?: boolean;
453
+ /** Optional system hint passed to connecting agents. */
454
+ instructions?: string;
455
+ /** Server name shown to clients; defaults to the site title. */
456
+ name?: string;
457
+ /** Route the server mounts at. Defaults to `/mcp`. */
458
+ route?: string;
459
+ }
460
+ /**
461
+ * AI-facing features: the Ask AI assistant, an `llms.txt` manifest, and the
462
+ * hosted MCP server.
463
+ */
450
464
  export interface AiConfig {
451
465
  /** The Ask AI chat assistant. */
452
466
  ask?: AskConfig;
@@ -474,6 +488,8 @@ export interface AiConfig {
474
488
  * ```
475
489
  */
476
490
  markdownComponents?: Record<string, ComponentMarkdown>;
491
+ /** Expose the docs as an MCP server for agents. */
492
+ mcp?: McpConfig;
477
493
  }
478
494
  /** An arbitrary analytics `<script>`; set exactly one of `src` or `content`. */
479
495
  export interface AnalyticsScript {
@@ -500,17 +516,6 @@ export interface AnalyticsConfig {
500
516
  /** Enable Vercel Web Analytics. */
501
517
  vercel?: boolean;
502
518
  }
503
- /** Expose the docs as an MCP server for connecting agents. */
504
- export interface McpConfig {
505
- /** Turn the MCP server on. Defaults to `false`. */
506
- enabled?: boolean;
507
- /** Optional system hint passed to connecting agents. */
508
- instructions?: string;
509
- /** Server name shown to clients; defaults to the site title. */
510
- name?: string;
511
- /** Route the server mounts at. Defaults to `/mcp`. */
512
- route?: string;
513
- }
514
519
  /** A configured locale plus display metadata for the switcher. */
515
520
  export interface LocaleConfigInput {
516
521
  /** Locale code, e.g. `en`, `fr`, `pt-BR`. */
@@ -829,8 +834,6 @@ export interface BlumeConfig {
829
834
  logo?: LogoConfig;
830
835
  /** Markdown / MDX rendering behavior. */
831
836
  markdown?: MarkdownConfig;
832
- /** Expose the docs as an MCP server for agents. */
833
- mcp?: McpConfig;
834
837
  /** Header, sidebar, tabs, and switchers. */
835
838
  navigation?: NavigationConfig;
836
839
  /** Native OpenAPI reference. */
@@ -66,9 +66,9 @@ import type { Diagnostic } from "./types.ts";
66
66
  * `algolia`, `typesense`, `orama-cloud`, `mixedbread`, or `none`) plus its
67
67
  * credential block.
68
68
  * - `ai` — `ask` (the Ask AI chat endpoint and its provider/model), `llmsTxt`
69
- * (emit `llms.txt`), and `markdownComponents` (Markdown serializers for
70
- * custom components in agent-facing output).
71
- * - `mcp` — expose the docs as an MCP server for connecting agents.
69
+ * (emit `llms.txt`), `mcp` (expose the docs as an MCP server for connecting
70
+ * agents), and `markdownComponents` (Markdown serializers for custom
71
+ * components in agent-facing output).
72
72
  *
73
73
  * **SEO, feeds & analytics**
74
74
  * - `seo` — `og` images, `sitemap`, `robots`, `rss` feeds, `structuredData`
@@ -819,12 +819,41 @@ declare const aiConfigSchema: z.ZodObject<{
819
819
  openapi?: boolean | undefined;
820
820
  } | undefined>;
821
821
  markdownComponents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodType<ComponentMarkdown, z.ZodTypeDef, ComponentMarkdown>>>;
822
+ /** Expose the docs as an MCP server for connecting agents. */
823
+ mcp: z.ZodDefault<z.ZodObject<{
824
+ enabled: z.ZodDefault<z.ZodBoolean>;
825
+ /** Optional system hint passed to connecting agents. */
826
+ instructions: z.ZodOptional<z.ZodString>;
827
+ /** Server name shown to clients; defaults to the site title. */
828
+ name: z.ZodOptional<z.ZodString>;
829
+ /**
830
+ * Normalized like `openapi.route`: a slash-less value would otherwise be
831
+ * string-concatenated onto the site origin (`https://acme.comdocs-mcp`).
832
+ */
833
+ route: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
834
+ }, "strict", z.ZodTypeAny, {
835
+ enabled: boolean;
836
+ route: string;
837
+ name?: string | undefined;
838
+ instructions?: string | undefined;
839
+ }, {
840
+ name?: string | undefined;
841
+ enabled?: boolean | undefined;
842
+ instructions?: string | undefined;
843
+ route?: string | undefined;
844
+ }>>;
822
845
  }, "strict", z.ZodTypeAny, {
823
846
  llmsTxt: {
824
847
  enabled: boolean;
825
848
  openapi: boolean;
826
849
  };
827
850
  markdownComponents: Record<string, ComponentMarkdown>;
851
+ mcp: {
852
+ enabled: boolean;
853
+ route: string;
854
+ name?: string | undefined;
855
+ instructions?: string | undefined;
856
+ };
828
857
  ask?: {
829
858
  enabled: boolean;
830
859
  model: string;
@@ -853,6 +882,12 @@ declare const aiConfigSchema: z.ZodObject<{
853
882
  openapi?: boolean | undefined;
854
883
  } | undefined;
855
884
  markdownComponents?: Record<string, ComponentMarkdown> | undefined;
885
+ mcp?: {
886
+ name?: string | undefined;
887
+ enabled?: boolean | undefined;
888
+ instructions?: string | undefined;
889
+ route?: string | undefined;
890
+ } | undefined;
856
891
  }>;
857
892
  export type AskAiProvider = (typeof askAiProviders)[number];
858
893
  export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
@@ -1075,12 +1110,41 @@ export declare const blumeConfigSchema: z.ZodObject<{
1075
1110
  openapi?: boolean | undefined;
1076
1111
  } | undefined>;
1077
1112
  markdownComponents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodType<ComponentMarkdown, z.ZodTypeDef, ComponentMarkdown>>>;
1113
+ /** Expose the docs as an MCP server for connecting agents. */
1114
+ mcp: z.ZodDefault<z.ZodObject<{
1115
+ enabled: z.ZodDefault<z.ZodBoolean>;
1116
+ /** Optional system hint passed to connecting agents. */
1117
+ instructions: z.ZodOptional<z.ZodString>;
1118
+ /** Server name shown to clients; defaults to the site title. */
1119
+ name: z.ZodOptional<z.ZodString>;
1120
+ /**
1121
+ * Normalized like `openapi.route`: a slash-less value would otherwise be
1122
+ * string-concatenated onto the site origin (`https://acme.comdocs-mcp`).
1123
+ */
1124
+ route: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
1125
+ }, "strict", z.ZodTypeAny, {
1126
+ enabled: boolean;
1127
+ route: string;
1128
+ name?: string | undefined;
1129
+ instructions?: string | undefined;
1130
+ }, {
1131
+ name?: string | undefined;
1132
+ enabled?: boolean | undefined;
1133
+ instructions?: string | undefined;
1134
+ route?: string | undefined;
1135
+ }>>;
1078
1136
  }, "strict", z.ZodTypeAny, {
1079
1137
  llmsTxt: {
1080
1138
  enabled: boolean;
1081
1139
  openapi: boolean;
1082
1140
  };
1083
1141
  markdownComponents: Record<string, ComponentMarkdown>;
1142
+ mcp: {
1143
+ enabled: boolean;
1144
+ route: string;
1145
+ name?: string | undefined;
1146
+ instructions?: string | undefined;
1147
+ };
1084
1148
  ask?: {
1085
1149
  enabled: boolean;
1086
1150
  model: string;
@@ -1109,6 +1173,12 @@ export declare const blumeConfigSchema: z.ZodObject<{
1109
1173
  openapi?: boolean | undefined;
1110
1174
  } | undefined;
1111
1175
  markdownComponents?: Record<string, ComponentMarkdown> | undefined;
1176
+ mcp?: {
1177
+ name?: string | undefined;
1178
+ enabled?: boolean | undefined;
1179
+ instructions?: string | undefined;
1180
+ route?: string | undefined;
1181
+ } | undefined;
1112
1182
  }>>;
1113
1183
  analytics: z.ZodOptional<z.ZodObject<{
1114
1184
  posthog: z.ZodOptional<z.ZodObject<{
@@ -1895,28 +1965,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
1895
1965
  headingAnchors?: boolean | undefined;
1896
1966
  imageZoom?: boolean | undefined;
1897
1967
  }>>;
1898
- mcp: z.ZodDefault<z.ZodObject<{
1899
- enabled: z.ZodDefault<z.ZodBoolean>;
1900
- /** Optional system hint passed to connecting agents. */
1901
- instructions: z.ZodOptional<z.ZodString>;
1902
- /** Server name shown to clients; defaults to the site title. */
1903
- name: z.ZodOptional<z.ZodString>;
1904
- /**
1905
- * Normalized like `openapi.route`: a slash-less value would otherwise be
1906
- * string-concatenated onto the site origin (`https://acme.comdocs-mcp`).
1907
- */
1908
- route: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
1909
- }, "strict", z.ZodTypeAny, {
1910
- enabled: boolean;
1911
- route: string;
1912
- name?: string | undefined;
1913
- instructions?: string | undefined;
1914
- }, {
1915
- name?: string | undefined;
1916
- enabled?: boolean | undefined;
1917
- route?: string | undefined;
1918
- instructions?: string | undefined;
1919
- }>>;
1920
1968
  navigation: z.ZodDefault<z.ZodObject<{
1921
1969
  /** Pinned links shown above the generated sidebar sections. */
1922
1970
  featured: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -2692,6 +2740,12 @@ export declare const blumeConfigSchema: z.ZodObject<{
2692
2740
  openapi: boolean;
2693
2741
  };
2694
2742
  markdownComponents: Record<string, ComponentMarkdown>;
2743
+ mcp: {
2744
+ enabled: boolean;
2745
+ route: string;
2746
+ name?: string | undefined;
2747
+ instructions?: string | undefined;
2748
+ };
2695
2749
  ask?: {
2696
2750
  enabled: boolean;
2697
2751
  model: string;
@@ -2835,12 +2889,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
2835
2889
  headingAnchors: boolean;
2836
2890
  imageZoom: boolean;
2837
2891
  };
2838
- mcp: {
2839
- enabled: boolean;
2840
- route: string;
2841
- name?: string | undefined;
2842
- instructions?: string | undefined;
2843
- };
2844
2892
  navigation: {
2845
2893
  sidebar: {
2846
2894
  display: "flat" | "page" | "group";
@@ -3025,6 +3073,12 @@ export declare const blumeConfigSchema: z.ZodObject<{
3025
3073
  openapi?: boolean | undefined;
3026
3074
  } | undefined;
3027
3075
  markdownComponents?: Record<string, ComponentMarkdown> | undefined;
3076
+ mcp?: {
3077
+ name?: string | undefined;
3078
+ enabled?: boolean | undefined;
3079
+ instructions?: string | undefined;
3080
+ route?: string | undefined;
3081
+ } | undefined;
3028
3082
  } | undefined;
3029
3083
  content?: {
3030
3084
  root?: string | undefined;
@@ -3206,12 +3260,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
3206
3260
  headingAnchors?: boolean | undefined;
3207
3261
  imageZoom?: boolean | undefined;
3208
3262
  } | undefined;
3209
- mcp?: {
3210
- name?: string | undefined;
3211
- enabled?: boolean | undefined;
3212
- route?: string | undefined;
3213
- instructions?: string | undefined;
3214
- } | undefined;
3215
3263
  navigation?: {
3216
3264
  sidebar?: SidebarItemConfig[] | {
3217
3265
  display?: "flat" | "page" | "group" | undefined;
@@ -222,6 +222,8 @@ const { config } = data;
222
222
  </PageLayout>
223
223
  ```
224
224
 
225
+ The header a custom page gets is the same one the docs pages get, so the chrome that lives in it comes along: search, the theme toggle, the language switcher, and — when [Ask AI](/docs/configuration/ai) is configured — the Ask AI trigger. None of it needs wiring up per page. Pass `askEnabled={false}` to leave the Ask trigger off one page while keeping it everywhere else.
226
+
225
227
  Passing `siteUrl` (and `ogEnabled`) derives the page's `canonical` and a generated `og:image` automatically: Blume renders an Open Graph card for every static custom page — the home included, the most-shared URL — served at `/og/<route>.png` (`/og/index.png` for `/`). The home card uses the site title with the description as its eyebrow; a deeper page is titled from its last path segment. Set `ogImage` or `canonical` explicitly to override either. `ogImage` takes a root-relative path — a file in `public/`, resolved against [`deployment.site`](/docs/deployment) to the absolute URL crawlers need — or an external URL, which passes through untouched:
226
228
 
227
229
  ```astro pages/index.astro lineNumbers
@@ -198,9 +198,11 @@ The `POST /api/ask` endpoint is **unauthenticated** — it has to be, so the in-
198
198
  Host a [Model Context Protocol](https://modelcontextprotocol.io) server so coding agents (Claude Code, Cursor, VS Code, claude.ai connectors) can search and read your docs directly — no scraping:
199
199
 
200
200
  ```ts blume.config.ts lineNumbers
201
- mcp: {
202
- enabled: true,
203
- route: "/mcp", // where the server is mounted
201
+ ai: {
202
+ mcp: {
203
+ enabled: true,
204
+ route: "/mcp", // where the server is mounted
205
+ },
204
206
  }
205
207
  ```
206
208
 
@@ -227,7 +229,7 @@ deployment: {
227
229
  }
228
230
  ```
229
231
 
230
- A static build with `mcp.enabled` fails fast with a message telling you to set `deployment.output` to `server`. See [Deployment](/docs/deployment) for the adapters. Once deployed, connect from Claude Code with:
232
+ A static build with `ai.mcp.enabled` fails fast with a message telling you to set `deployment.output` to `server`. See [Deployment](/docs/deployment) for the adapters. Once deployed, connect from Claude Code with:
231
233
 
232
234
  ```bash
233
235
  claude mcp add --transport http my-docs https://docs.example.com/mcp
@@ -64,12 +64,11 @@ export default defineConfig({
64
64
  // AI — see the AI guide
65
65
  ai: {
66
66
  llmsTxt: true,
67
- },
68
-
69
- // MCP server (needs server output) — see the AI guide
70
- mcp: {
71
- enabled: false,
72
- route: "/mcp",
67
+ // MCP server (needs server output)
68
+ mcp: {
69
+ enabled: false,
70
+ route: "/mcp",
71
+ },
73
72
  },
74
73
 
75
74
  // SEO — OG images, feeds, sitemap, structured data; see the SEO guide
@@ -287,8 +286,7 @@ Each of these has its own guide. The config field is the entry point:
287
286
  | `navigation` | Explicit sidebar and header tabs | [Navigation](/docs/content/navigation) |
288
287
  | `search` | Provider (Orama, Pagefind, Algolia, and more) and indexing | [Search](/docs/configuration/search) |
289
288
  | `markdown` | Markdown rendering options — code blocks, heading anchors, image zoom | [Syntax](/docs/content/syntax) |
290
- | `ai` | `llms.txt`, Ask AI, and the MCP server | [AI](/docs/configuration/ai) |
291
- | `mcp` | Hosted MCP server for coding agents | [AI](/docs/configuration/ai#mcp-server) |
289
+ | `ai` | `llms.txt`, Ask AI, and the hosted MCP server for coding agents | [AI](/docs/configuration/ai) |
292
290
  | `analytics` | Vercel, PostHog, and custom scripts | [Analytics](/docs/configuration/analytics) |
293
291
  | `seo` | Metadata, OG images, feeds, structured data | [SEO](/docs/configuration/seo) |
294
292
  | `deployment` | Output mode, adapter, and site URL | [Deployment](/docs/deployment) |
@@ -532,14 +532,14 @@ export interface ButtonProps {
532
532
 
533
533
  A card linking to a GitHub repository with its live star and fork counts. Counts are fetched at build time — no client JavaScript — and the card still renders if the API is unreachable. Pass `owner` and `repo`, or omit them to use the repository from your `blume.config`. Set a `GITHUB_TOKEN` environment variable to lift the API rate limit.
534
534
 
535
- <GithubInfo owner="withastro" repo="astro" />
535
+ <GithubInfo owner="haydenbleasel" repo="blume" />
536
536
 
537
537
  ```astro lineNumbers
538
538
  <!-- Uses the repo from blume.config -->
539
539
  <GithubInfo />
540
540
 
541
541
  <!-- Or point it at any repository -->
542
- <GithubInfo owner="withastro" repo="astro" />
542
+ <GithubInfo owner="haydenbleasel" repo="blume" />
543
543
  ```
544
544
 
545
545
  ## Component
@@ -670,6 +670,28 @@ Diff two files in your project, relative to its root:
670
670
 
671
671
  <Diff before="diffs/button-before.ts" after="diffs/button-after.ts" />
672
672
 
673
- Or render a unified patch from a file:
673
+ ```astro
674
+ <Diff before="diffs/button-before.ts" after="diffs/button-after.ts" />
675
+ ```
676
+
677
+ Or render a unified patch — either from a file with `src`, or inline with `patch`:
674
678
 
675
679
  <Diff src="diffs/greet.patch" />
680
+
681
+ ```astro
682
+ <Diff src="diffs/greet.patch" />
683
+ ```
684
+
685
+ ```astro
686
+ <Diff
687
+ patch={`--- a/greet.ts
688
+ +++ b/greet.ts
689
+ @@ -1,3 +1,4 @@
690
+ -export function greet(name) {
691
+ - return "Hi, " + name;
692
+ +export function greet(name: string): string {
693
+ + const greeting = "Hi, " + name + "!";
694
+ + return greeting;
695
+ }`}
696
+ />
697
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -62,10 +62,10 @@ export const buildAgentReadability = (
62
62
  artifacts.llmsFullTxt = abs("/llms-full.txt");
63
63
  artifacts.llmsTxt = abs("/llms.txt");
64
64
  }
65
- if (config.mcp.enabled) {
65
+ if (config.ai.mcp.enabled) {
66
66
  artifacts.mcp = {
67
67
  discovery: abs("/.well-known/mcp.json"),
68
- url: abs(config.mcp.route),
68
+ url: abs(config.ai.mcp.route),
69
69
  };
70
70
  }
71
71
  if (config.ai.ask?.enabled) {
@@ -87,7 +87,7 @@ export const buildAgentReadability = (
87
87
  artifacts,
88
88
  description: config.description,
89
89
  generator: version ? `blume@${version}` : undefined,
90
- name: config.mcp.name ?? config.title,
90
+ name: config.ai.mcp.name ?? config.title,
91
91
  site,
92
92
  };
93
93
 
@@ -88,8 +88,8 @@ export const buildMcpData = async (project: BlumeProject): Promise<McpData> => {
88
88
  route: doc.route,
89
89
  title: doc.title,
90
90
  })),
91
- instructions: config.mcp.instructions,
92
- name: config.mcp.name ?? config.title,
91
+ instructions: config.ai.mcp.instructions,
92
+ name: config.ai.mcp.name ?? config.title,
93
93
  navigation: graph.navigation,
94
94
  pages,
95
95
  routes,
@@ -61,6 +61,7 @@ import {
61
61
  routeIsTaken,
62
62
  } from "./pages.ts";
63
63
  import {
64
+ askComponentTemplate,
64
65
  askEndpointTemplate,
65
66
  astroConfigTemplate,
66
67
  catchAllPageTemplate,
@@ -865,8 +866,11 @@ export const buildRuntimeData = (project: BlumeProject): string => {
865
866
  : null,
866
867
  imageZoom: config.markdown.imageZoom,
867
868
  logo: resolveLogo(project),
868
- mcp: config.mcp.enabled
869
- ? { name: config.mcp.name ?? config.title, route: config.mcp.route }
869
+ mcp: config.ai.mcp.enabled
870
+ ? {
871
+ name: config.ai.mcp.name ?? config.title,
872
+ route: config.ai.mcp.route,
873
+ }
870
874
  : null,
871
875
  // `og.enabled` is resolved to a definite boolean in `loadConfig`; coerce
872
876
  // the optional schema type so the serialized shape stays `boolean`.
@@ -939,7 +943,7 @@ const planMcp = (
939
943
  userPages: { pattern: string }[]
940
944
  ): McpPlan => {
941
945
  const { config } = project;
942
- const { route } = config.mcp;
946
+ const { route } = config.ai.mcp;
943
947
  const dir = join(srcDir, "blume-mcp");
944
948
  const base: McpPlan = {
945
949
  dir,
@@ -949,14 +953,14 @@ const planMcp = (
949
953
  srcDir,
950
954
  warnings: [],
951
955
  };
952
- if (!config.mcp.enabled) {
956
+ if (!config.ai.mcp.enabled) {
953
957
  return base;
954
958
  }
955
959
  if (routeIsTaken(userPages, project.graph.pages, route)) {
956
960
  return {
957
961
  ...base,
958
962
  warnings: [
959
- `MCP server route "${route}" is already used by a content or custom page; the MCP server was not generated. Set a different "mcp.route" in blume.config.ts.`,
963
+ `MCP server route "${route}" is already used by a content or custom page; the MCP server was not generated. Set a different "ai.mcp.route" in blume.config.ts.`,
960
964
  ],
961
965
  };
962
966
  }
@@ -1105,6 +1109,7 @@ export const generateRuntime = async (
1105
1109
  const { context, config } = project;
1106
1110
  const out = context.outDir;
1107
1111
  const srcDir = join(out, "src");
1112
+ const askPath = join(srcDir, "generated", "Ask.astro");
1108
1113
  const dataPath = join(srcDir, "generated", "data.json");
1109
1114
  const themePath = join(srcDir, "generated", "app.css");
1110
1115
  const searchClientPath = join(srcDir, "generated", "search-client.ts");
@@ -1205,6 +1210,7 @@ export const generateRuntime = async (
1205
1210
  join(out, "astro.config.mjs"),
1206
1211
  astroConfigTemplate({
1207
1212
  aliases: resolveTsconfigAliases(context.root),
1213
+ askPath,
1208
1214
  config,
1209
1215
  contentRoutes: project.manifest.routes.map((route) => route.path),
1210
1216
  context,
@@ -1242,13 +1248,16 @@ export const generateRuntime = async (
1242
1248
  write(
1243
1249
  join(srcDir, "pages", "[...slug].astro"),
1244
1250
  catchAllPageTemplate({
1245
- askEnabled,
1246
1251
  exportEpub,
1247
1252
  exportPdf,
1248
1253
  mathEnabled: usesMath,
1249
1254
  needsReact,
1250
1255
  })
1251
1256
  ),
1257
+ // The header's Ask trigger, behind the `blume:ask` alias. Always written
1258
+ // (even when Ask is off, as a component that renders nothing) so the alias
1259
+ // resolves — the same contract as `blume:search-client`.
1260
+ write(askPath, askComponentTemplate(askEnabled)),
1252
1261
  write(join(srcDir, "generated", "components.ts"), slotPlan.module),
1253
1262
  write(
1254
1263
  join(srcDir, "generated", "islands.ts"),
@@ -1336,7 +1345,6 @@ export const generateRuntime = async (
1336
1345
  await write(
1337
1346
  join(srcDir, "pages", "changelog.astro"),
1338
1347
  changelogIndexTemplate({
1339
- askEnabled,
1340
1348
  exportEpub,
1341
1349
  exportPdf,
1342
1350
  needsReact,
@@ -222,6 +222,8 @@ export const astroConfigTemplate = (options: {
222
222
  needsSvelte?: boolean;
223
223
  pages: BlumePageRoute[];
224
224
  contentRoutes: string[];
225
+ /** The generated Ask trigger (`blume:ask`); renders nothing when Ask is off. */
226
+ askPath: string;
225
227
  dataPath: string;
226
228
  examplesPath: string;
227
229
  /** The example-preview Tailwind entry (`blume:examples-theme`). */
@@ -240,6 +242,7 @@ export const astroConfigTemplate = (options: {
240
242
  }): string => {
241
243
  const { context, config, needsReact, pages, dataPath, themePath } = options;
242
244
  const {
245
+ askPath,
243
246
  contentRoutes,
244
247
  examplesPath,
245
248
  examplesThemePath,
@@ -425,6 +428,7 @@ export default defineConfig({
425
428
  },
426
429
  resolve: {
427
430
  alias: {
431
+ "blume:ask": ${JSON.stringify(askPath)},
428
432
  "blume:data": ${JSON.stringify(dataPath)},
429
433
  "blume:examples": ${JSON.stringify(examplesPath)},
430
434
  "blume:examples-theme": ${JSON.stringify(examplesThemePath)},
@@ -694,6 +698,42 @@ ${handler}
694
698
  `;
695
699
  };
696
700
 
701
+ /**
702
+ * Generate `.blume/src/generated/Ask.astro` — the component behind the
703
+ * `blume:ask` alias that the shared header renders in place of a per-page slot.
704
+ *
705
+ * The header can't import the Ask AI island directly: it's a React component, so
706
+ * the import alone would drag the JSX renderer into the module graph of every
707
+ * project — including the ones that never enable Ask AI and therefore have no
708
+ * React integration wired into their generated Astro config (see `needsReact`).
709
+ * Routing the import through a generated component keeps that dependency behind
710
+ * the config switch: enabled projects get the island, disabled ones get a
711
+ * component that renders nothing and imports no React.
712
+ *
713
+ * `strings` comes from the header (the active locale's dictionary); the empty-
714
+ * state suggestions are read straight from the data snapshot, which is why no
715
+ * page has to pass them.
716
+ */
717
+ export const askComponentTemplate = (askEnabled: boolean): string =>
718
+ askEnabled
719
+ ? `---
720
+ // Generated by Blume. Do not edit.
721
+ import AskAI from "blume/components/islands/AskAI.astro";
722
+ import data from "blume:data";
723
+
724
+ const { strings } = Astro.props;
725
+ ---
726
+
727
+ <AskAI strings={strings ?? data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />
728
+ `
729
+ : `---
730
+ // Generated by Blume. Do not edit.
731
+ // Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
732
+ // Deliberately imports no React island, keeping the JSX renderer out of projects
733
+ // that don't need it.
734
+ ---
735
+ `;
736
+
697
737
  /** Generate the static search index endpoint (`/blume-search.json`). */
698
738
  export const searchEndpointTemplate = (): string =>
699
739
  `// Generated by Blume. Do not edit.
@@ -1069,19 +1109,12 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
1069
1109
  `;
1070
1110
 
1071
1111
  export const catchAllPageTemplate = (options: {
1072
- askEnabled: boolean;
1073
1112
  exportEpub: boolean;
1074
1113
  exportPdf: boolean;
1075
1114
  mathEnabled: boolean;
1076
1115
  /** Serialize the island-hooks snapshot; only needed when React is enabled. */
1077
1116
  needsReact: boolean;
1078
1117
  }): string => {
1079
- const askImport = options.askEnabled
1080
- ? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
1081
- : "";
1082
- const askSlot = options.askEnabled
1083
- ? '\n <AskAI slot="ask" strings={ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
1084
- : "";
1085
1118
  const mathImport = options.mathEnabled
1086
1119
  ? 'import Math from "blume/components/content/Math.astro";\n'
1087
1120
  : "";
@@ -1097,7 +1130,6 @@ import { getEntry, render } from "astro:content";
1097
1130
  import RootLayout from "blume/components/layout/RootLayout.astro";
1098
1131
  import { withBase } from "blume/components/islands/base-path.ts";
1099
1132
  import { resolveSlot } from "blume/components/layout/overrides.ts";
1100
- ${askImport}
1101
1133
  import Accordion from "blume/components/content/Accordion.astro";
1102
1134
  import AccordionItem from "blume/components/content/AccordionItem.astro";
1103
1135
  import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
@@ -1334,7 +1366,6 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
1334
1366
  canonical={canonical}
1335
1367
  editUrl={editUrl}
1336
1368
  feedback={data.config.feedback}
1337
- askEnabled={${options.askEnabled}}
1338
1369
  exportPdf={${options.exportPdf}}
1339
1370
  exportEpub={${options.exportEpub}}
1340
1371
  feeds={data.feeds}
@@ -1344,7 +1375,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
1344
1375
  lastModified={lastModified}
1345
1376
  noindex={seo.noindex}
1346
1377
  structuredDataEnabled={data.config.structuredData}
1347
- >${askSlot}
1378
+ >
1348
1379
  <h1>{title}</h1>
1349
1380
  {frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
1350
1381
  <Content components={components} />
@@ -1359,7 +1390,6 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
1359
1390
  * by {@link generateAstroProject} when changelog entries exist.
1360
1391
  */
1361
1392
  export const changelogIndexTemplate = (options: {
1362
- askEnabled: boolean;
1363
1393
  exportEpub: boolean;
1364
1394
  exportPdf: boolean;
1365
1395
  /** Serialize the island-hooks snapshot; only needed when React is enabled. */
@@ -1367,12 +1397,6 @@ export const changelogIndexTemplate = (options: {
1367
1397
  /** Whether a `staged` collection exists (non-filesystem changelog sources). */
1368
1398
  staged: boolean;
1369
1399
  }): string => {
1370
- const askImport = options.askEnabled
1371
- ? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
1372
- : "";
1373
- const askSlot = options.askEnabled
1374
- ? '\n <AskAI slot="ask" strings={data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
1375
- : "";
1376
1400
  const clientData = options.needsReact
1377
1401
  ? '\n clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: pageTitle } }}'
1378
1402
  : "";
@@ -1390,7 +1414,7 @@ import Update from "blume/components/content/Update.astro";
1390
1414
  import { withBase } from "blume/components/islands/base-path.ts";
1391
1415
  import { resolveSlot } from "blume/components/layout/overrides.ts";
1392
1416
  import { layoutOverrides } from "../generated/components.ts";
1393
- ${askImport}import data from "../generated/data.json";
1417
+ import data from "../generated/data.json";
1394
1418
 
1395
1419
  export const prerender = true;
1396
1420
 
@@ -1559,14 +1583,13 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
1559
1583
  ogImage={null}
1560
1584
  x={data.config.x}
1561
1585
  canonical={canonical}
1562
- askEnabled={${options.askEnabled}}
1563
1586
  exportPdf={${options.exportPdf}}
1564
1587
  exportEpub={${options.exportEpub}}
1565
1588
  feeds={data.feeds}
1566
1589
  siteUrl={data.config.site}
1567
1590
  noindex={false}
1568
1591
  structuredDataEnabled={data.config.structuredData}
1569
- >${askSlot}
1592
+ >
1570
1593
  <h1>{changelogTitle}</h1>
1571
1594
  {
1572
1595
  items.length === 0 ? (
@@ -1891,6 +1914,11 @@ export const envTemplate =
1891
1914
  (): string => `/// <reference path="../.astro/types.d.ts" />
1892
1915
  /// <reference types="astro/client" />
1893
1916
 
1917
+ declare module "blume:ask" {
1918
+ const Ask: typeof import("blume/components/islands/AskAI.astro").default;
1919
+ export default Ask;
1920
+ }
1921
+
1894
1922
  declare module "blume:data" {
1895
1923
  const data: import("blume").BlumeData;
1896
1924
  export default data;