blume 1.6.2 → 1.6.4

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 (79) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli/index.js +307 -68
  3. package/dist/cli/index.js.map +28 -23
  4. package/dist/types/ai/component-markdown.d.ts +14 -0
  5. package/docs/01-quickstart.mdx +2 -2
  6. package/docs/02-deployment.mdx +5 -5
  7. package/docs/{07-faq.mdx → 08-faq.mdx} +7 -7
  8. package/docs/advanced/blog.mdx +3 -3
  9. package/docs/advanced/changelog.mdx +2 -2
  10. package/docs/advanced/custom-pages.mdx +4 -4
  11. package/docs/advanced/meta.ts +1 -1
  12. package/docs/configuration/ask-ai.mdx +179 -0
  13. package/docs/configuration/index.mdx +8 -7
  14. package/docs/configuration/meta.ts +1 -2
  15. package/docs/configuration/search.mdx +1 -1
  16. package/docs/configuration/theming.mdx +1 -1
  17. package/docs/content/components.mdx +1 -1
  18. package/docs/content/i18n.mdx +7 -1
  19. package/docs/content/index.mdx +1 -1
  20. package/docs/content/navigation.mdx +2 -2
  21. package/docs/content/syntax.mdx +1 -1
  22. package/docs/discoverability/agent-discovery.mdx +196 -0
  23. package/docs/discoverability/index.mdx +48 -0
  24. package/docs/discoverability/json-api.mdx +58 -0
  25. package/docs/discoverability/llms-txt.mdx +68 -0
  26. package/docs/discoverability/markdown.mdx +76 -0
  27. package/docs/discoverability/mcp.mdx +64 -0
  28. package/docs/discoverability/meta.ts +18 -0
  29. package/docs/discoverability/metadata.mdx +82 -0
  30. package/docs/discoverability/open-graph.mdx +113 -0
  31. package/docs/discoverability/rss.mdx +24 -0
  32. package/docs/discoverability/sitemap-and-robots.mdx +95 -0
  33. package/docs/discoverability/structured-data.mdx +51 -0
  34. package/docs/index.mdx +5 -5
  35. package/docs/reference/eval.mdx +1 -1
  36. package/docs/reference/meta.ts +1 -1
  37. package/docs/reference/translate.mdx +1 -0
  38. package/package.json +18 -18
  39. package/src/ai/component-markdown.ts +17 -2
  40. package/src/ai/llms.ts +3 -10
  41. package/src/ai/markdown.ts +3 -10
  42. package/src/ai/openapi-components.ts +123 -0
  43. package/src/ai/serializers.ts +24 -0
  44. package/src/astro/templates.ts +42 -12
  45. package/src/audit/checks/links.ts +1 -8
  46. package/src/audit/checks/llms.ts +5 -4
  47. package/src/audit/redirects.ts +4 -3
  48. package/src/audit/run.ts +6 -8
  49. package/src/audit/url.ts +33 -0
  50. package/src/cli/commands/validate.ts +1 -0
  51. package/src/components/content/Component.astro +65 -68
  52. package/src/components/content/Tabs.astro +24 -9
  53. package/src/components/content/example-pane.ts +6 -0
  54. package/src/components/layout/LocaleLinks.astro +42 -0
  55. package/src/components/layout/PageLayout.astro +5 -3
  56. package/src/components/layout/ReferenceLayout.astro +5 -0
  57. package/src/components/layout/RootLayout.astro +110 -39
  58. package/src/components/layout/search-locale.ts +13 -0
  59. package/src/components/openapi/ApiOverview.astro +7 -39
  60. package/src/components/openapi/ApiTagOperations.astro +2 -1
  61. package/src/components/openapi/AsyncApiOperation.astro +3 -2
  62. package/src/components/openapi/GraphqlOperation.astro +3 -2
  63. package/src/components/openapi/Operation.astro +3 -2
  64. package/src/core/i18n.ts +13 -2
  65. package/src/core/links.ts +33 -1
  66. package/src/core/locale-links.ts +163 -0
  67. package/src/core/sources/normalize.ts +57 -7
  68. package/src/markdown/package-commands.ts +27 -3
  69. package/src/openapi/graphql.ts +29 -0
  70. package/src/openapi/model.ts +69 -0
  71. package/src/openapi/render-mdx.ts +3 -2
  72. package/src/openapi/signature.ts +18 -0
  73. package/src/search/documents.ts +4 -9
  74. package/src/theme/code-block-padding.ts +0 -8
  75. package/src/theme/entry.ts +33 -27
  76. package/src/translate/anchors.ts +91 -0
  77. package/src/translate/validate.ts +8 -3
  78. package/docs/configuration/ai.mdx +0 -613
  79. package/docs/configuration/seo.mdx +0 -364
@@ -46,6 +46,7 @@ interface MdxAttribute {
46
46
  export type EvaluatedValue = string | number | boolean | null | undefined | Date | EvaluatedValue[] | {
47
47
  [key: string]: EvaluatedValue;
48
48
  };
49
+ export declare const isString: <Value>(value: Value) => value is Value & string;
49
50
  /** Evaluated props plus whether any attribute resisted static evaluation. */
50
51
  interface EvaluatedProps {
51
52
  lossy: boolean;
@@ -94,6 +95,19 @@ export interface ComponentMarkdownContext extends EvaluatedProps {
94
95
  * fallback when the props can't be recovered statically).
95
96
  */
96
97
  export type ComponentMarkdown = (context: ComponentMarkdownContext) => string | null;
98
+ /**
99
+ * A link destination. Whitespace ends one, and a `)` ends one unless it is
100
+ * part of a balanced pair — so an href carrying either goes in the angle
101
+ * bracket form, where only `<` and `>` are special.
102
+ */
103
+ export declare const linkDestination: (href: string) => string;
104
+ /**
105
+ * `text` as an inline code span whose delimiter outlengths any backtick run
106
+ * inside it, padded with a space when the content starts or ends with a
107
+ * backtick (CommonMark strips one such pair) — so a spec path or address
108
+ * carrying a backtick cannot close the span early.
109
+ */
110
+ export declare const inlineCode: (text: string) => string;
97
111
  /**
98
112
  * Build the `<Component>` serializer for a project's discovered examples. The
99
113
  * live preview can't survive the trip to Markdown, so the agent-facing output
@@ -86,8 +86,8 @@ Save it, and the dev server reloads instantly. Navigation, search, and page meta
86
86
  <Card title="Components" href="/docs/content/components" icon="folder">
87
87
  Explore the built-in component library.
88
88
  </Card>
89
- <Card title="AI" href="/docs/configuration/ai" icon="lightbulb">
90
- Ship `llms.txt` and an Ask AI assistant.
89
+ <Card title="Discoverability" href="/docs/discoverability" icon="lightbulb">
90
+ Ship `llms.txt`, social cards, and an MCP server.
91
91
  </Card>
92
92
  <Card title="Deployment" href="/docs/deployment" icon="rocket">
93
93
  Go live on any static host.
@@ -22,10 +22,10 @@ A static build includes:
22
22
 
23
23
  - every docs and custom page as static HTML
24
24
  - a local search index (Orama by default, Pagefind opt-in)
25
- - a [`sitemap.xml`](/docs/configuration/seo#sitemap) and [`robots.txt`](/docs/configuration/seo#robots) when `deployment.site` is set
25
+ - a [`sitemap.xml`](/docs/discoverability/sitemap-and-robots#sitemap) and [`robots.txt`](/docs/discoverability/sitemap-and-robots#robots) when `deployment.site` is set
26
26
  - `llms.txt` and `llms-full.txt` for AI tools
27
27
  - redirect pages
28
- - prerendered [Open Graph images](/docs/configuration/seo#open-graph-images) when `seo.og.enabled` is on
28
+ - prerendered [Open Graph images](/docs/discoverability/open-graph) when `seo.og.enabled` is on
29
29
 
30
30
  ### Set your site URL
31
31
 
@@ -79,7 +79,7 @@ This is a distinct concept from the two paths above:
79
79
 
80
80
  ## Server rendering
81
81
 
82
- Static output covers most docs. Switch to server output when you need request-time features — most notably the [Ask AI](/docs/configuration/ai) endpoint:
82
+ Static output covers most docs. Switch to server output when you need request-time features — most notably the [Ask AI](/docs/configuration/ask-ai) endpoint:
83
83
 
84
84
  ```ts blume.config.ts lineNumbers
85
85
  deployment: {
@@ -101,10 +101,10 @@ On **Vercel**, **Netlify**, and **Cloudflare Pages**, Blume picks the matching a
101
101
 
102
102
  A server build includes everything a static build does, plus any Astro endpoints or middleware you add. The `node` adapter produces a standalone server you can run directly.
103
103
 
104
- On Vercel and Cloudflare, a server build also turns on [`Accept: text/markdown` content negotiation](/docs/configuration/ai#content-negotiation), so an agent requesting any content page with that header receives its raw-Markdown mirror at the same URL. On Vercel, Blume splices header-conditional rewrites into the deploy's routing config; on Cloudflare, it generates a small Worker in front of the Astro one and scopes `assets.run_worker_first` to the content routes, since the platform would otherwise serve the prerendered pages before any server code runs — other assets keep their zero-Worker fast path.
104
+ On Vercel and Cloudflare, a server build also turns on [`Accept: text/markdown` content negotiation](/docs/discoverability/markdown#content-negotiation), so an agent requesting any content page with that header receives its raw-Markdown mirror at the same URL. On Vercel, Blume splices header-conditional rewrites into the deploy's routing config; on Cloudflare, it generates a small Worker in front of the Astro one and scopes `assets.run_worker_first` to the content routes, since the platform would otherwise serve the prerendered pages before any server code runs — other assets keep their zero-Worker fast path.
105
105
 
106
106
  :::note
107
- Server features have their own configuration — for example, Ask AI needs a model API key. See the [AI guide](/docs/configuration/ai) for setup.
107
+ Server features have their own configuration — for example, Ask AI needs a model API key. See the [Ask AI guide](/docs/configuration/ask-ai) for setup.
108
108
  :::
109
109
 
110
110
  ## Redirects
@@ -87,13 +87,13 @@ We reported it upstream in [oxc-project/oxc#24096](https://github.com/oxc-projec
87
87
 
88
88
  Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships the same fix in its own repo, and you can apply it in any project.
89
89
 
90
- 1. Save the patch as `patches/oxfmt@0.66.0.patch`:
90
+ 1. Save the patch as `patches/oxfmt@0.67.0.patch`:
91
91
 
92
- ```diff patches/oxfmt@0.66.0.patch
93
- diff --git a/dist/markdown-BgZGxhM2.js b/dist/markdown-BgZGxhM2.js
94
- index 859231f9387a50df16419bc22b5268c4e446ddbc..dcc0ffda6f71adb27a03e3918a3db6fc7a58ffe9 100644
95
- --- a/dist/markdown-BgZGxhM2.js
96
- +++ b/dist/markdown-BgZGxhM2.js
92
+ ```diff patches/oxfmt@0.67.0.patch
93
+ diff --git a/dist/markdown-BMigo7Hm.js b/dist/markdown-BMigo7Hm.js
94
+ index bc9037f6c0de5516b139d8cdb195b1e25cd33bc0..a02c284e28bb535f9964a8a086ebb6549657e416 100644
95
+ --- a/dist/markdown-BMigo7Hm.js
96
+ +++ b/dist/markdown-BMigo7Hm.js
97
97
  @@ -4872,7 +4872,43 @@ function lu(e, t, r) {
98
98
  case "sentence": return Oh(e, r);
99
99
  case "word": return t.parser !== "mdx" ? zh(e, t) : Uh(e);
@@ -146,7 +146,7 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
146
146
  ```json package.json
147
147
  {
148
148
  "patchedDependencies": {
149
- "oxfmt@0.66.0": "patches/oxfmt@0.66.0.patch"
149
+ "oxfmt@0.67.0": "patches/oxfmt@0.67.0.patch"
150
150
  }
151
151
  }
152
152
  ```
@@ -26,7 +26,7 @@ Give every post a `date` so feed items sort newest-first and carry a `pubDate`,
26
26
 
27
27
  Blume builds a blog feed at **`/blog/rss.xml`**, sorted newest-first by `date`. Feeds need an absolute site URL, so set [`deployment.site`](/docs/deployment); Blume then injects a `<link rel="alternate">` tag on every page so readers discover it automatically.
28
28
 
29
- The feed is on by default. Tune it under [`seo.rss`](/docs/configuration/seo#rss-feeds):
29
+ The feed is on by default. Tune it under [`seo.rss`](/docs/discoverability/rss):
30
30
 
31
31
  ```ts blume.config.ts lineNumbers
32
32
  seo: {
@@ -42,7 +42,7 @@ Remove `"blog"` from `rss.types` to skip the feed.
42
42
 
43
43
  ## Structured data
44
44
 
45
- When [structured data](/docs/configuration/seo#structured-data) is on, each post is emitted as a schema.org **`BlogPosting`** with its description and publish date — the richer article type search engines expect for blog content.
45
+ When [structured data](/docs/discoverability/structured-data) is on, each post is emitted as a schema.org **`BlogPosting`** with its description and publish date — the richer article type search engines expect for blog content.
46
46
 
47
47
  ## Building an index
48
48
 
@@ -99,7 +99,7 @@ See [Custom Pages](/docs/advanced/custom-pages#using-the-site-layout) for wrappi
99
99
  <Card title="Custom Pages" href="/docs/advanced/custom-pages" icon="folder">
100
100
  Mount the blog index and read from blume:data.
101
101
  </Card>
102
- <Card title="SEO" href="/docs/configuration/seo" icon="file">
102
+ <Card title="Discoverability" href="/docs/discoverability" icon="file">
103
103
  Feeds, Open Graph images, and structured data.
104
104
  </Card>
105
105
  </CardGroup>
@@ -103,7 +103,7 @@ The release name becomes the title, its tag becomes `changelog.version`, and its
103
103
 
104
104
  Blume also builds a changelog feed at **`/changelog/rss.xml`**, sorted newest-first by `date`. Feeds need an absolute site URL, so set [`deployment.site`](/docs/deployment); Blume then injects a `<link rel="alternate">` tag on every page so readers discover it automatically.
105
105
 
106
- The feed is on by default. Tune it under [`seo.rss`](/docs/configuration/seo#rss-feeds):
106
+ The feed is on by default. Tune it under [`seo.rss`](/docs/discoverability/rss):
107
107
 
108
108
  ```ts blume.config.ts lineNumbers
109
109
  seo: {
@@ -119,7 +119,7 @@ Remove `"changelog"` from `rss.types` to skip the feed while keeping the timelin
119
119
 
120
120
  ## Structured data
121
121
 
122
- When [structured data](/docs/configuration/seo#structured-data) is on, each changelog entry is emitted as a schema.org **`TechArticle`** with its description and publish date, so search engines can index releases as dated articles.
122
+ When [structured data](/docs/discoverability/structured-data) is on, each changelog entry is emitted as a schema.org **`TechArticle`** with its description and publish date, so search engines can index releases as dated articles.
123
123
 
124
124
  <CardGroup cols={2}>
125
125
  <Card
@@ -222,7 +222,7 @@ 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.
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/ask-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
226
 
227
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:
228
228
 
@@ -241,7 +241,7 @@ Passing `siteUrl` (and `ogEnabled`) derives the page's `canonical` and a generat
241
241
 
242
242
  Only this page changes — every other route keeps its generated card — so it's how you give the home page alone a bespoke share image. The generated card declares its size and alt text to crawlers on its own; for your own `ogImage`, pass `ogImageAlt` and `ogImageSize` alongside so the share card gets the same treatment.
243
243
 
244
- The page also emits schema.org JSON-LD — the same `WebSite` graph the docs pages carry, so the home page (usually a custom page) isn't the one URL without structured data. Pass `structuredDataEnabled={config.structuredData}` to keep it in sync with the [`structuredData`](/docs/configuration/seo) config, or `structuredDataEnabled={false}` to turn it off for one page.
244
+ The page also emits schema.org JSON-LD — the same `WebSite` graph the docs pages carry, so the home page (usually a custom page) isn't the one URL without structured data. Pass `structuredDataEnabled={config.structuredData}` to keep it in sync with the [`structuredData`](/docs/discoverability/structured-data) config, or `structuredDataEnabled={false}` to turn it off for one page.
245
245
 
246
246
  `page.title` is used verbatim as the document title (no `- siteTitle` suffix), since marketing pages usually set their own. To give a custom page the full docs chrome instead — sidebar, TOC, and all — wrap it in `RootLayout`, the layout the generated pages use. Pull the required props straight from `blume:data`:
247
247
 
@@ -272,9 +272,9 @@ import data from "blume:data";
272
272
 
273
273
  ## 404 page
274
274
 
275
- Blume ships a default **not found** page out of the box: a centered "404" message wrapped in the site chrome (header, search, theme), served for any unmatched URL. `blume build` writes it to `404.html`, which static hosts serve automatically, and `blume dev` shows it for unknown routes. Under the message, a **Where to look next** list links every top-level section plus the `sitemap.xml` and [`llms.txt`](/docs/configuration/ai#llmstxt) indexes when they exist, so a reader — or an agent that followed a stale URL — has a way back.
275
+ Blume ships a default **not found** page out of the box: a centered "404" message wrapped in the site chrome (header, search, theme), served for any unmatched URL. `blume build` writes it to `404.html`, which static hosts serve automatically, and `blume dev` shows it for unknown routes. Under the message, a **Where to look next** list links every top-level section plus the `sitemap.xml` and [`llms.txt`](/docs/discoverability/llms-txt) indexes when they exist, so a reader — or an agent that followed a stale URL — has a way back.
276
276
 
277
- The page also has a Markdown twin at `/404.md` and a JSON twin at `/404.json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details) with the same recovery links (absolute URLs once [`deployment.site`](/docs/deployment) is set), plus the [`openapi.json`](/docs/configuration/ai#json-api) description when the JSON API is on. On a [Vercel server build](/docs/deployment#server-rendering), a request for a missing page that sends [`Accept: text/markdown`](/docs/configuration/ai#content-negotiation), or asks for a `.md` URL no page backs, gets the Markdown body with the `404` status instead of the HTML shell; one that sends `Accept: application/json`, or asks for a `.json` URL no file backs, gets the problem document — so an agent never has to parse a page of chrome to learn where to go next.
277
+ The page also has a Markdown twin at `/404.md` and a JSON twin at `/404.json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details) with the same recovery links (absolute URLs once [`deployment.site`](/docs/deployment) is set), plus the [`openapi.json`](/docs/discoverability/json-api) description when the JSON API is on. On a [Vercel server build](/docs/deployment#server-rendering), a request for a missing page that sends [`Accept: text/markdown`](/docs/discoverability/markdown#content-negotiation), or asks for a `.md` URL no page backs, gets the Markdown body with the `404` status instead of the HTML shell; one that sends `Accept: application/json`, or asks for a `.json` URL no file backs, gets the problem document — so an agent never has to parse a page of chrome to learn where to go next.
278
278
 
279
279
  To replace it with your own, add a `pages/404.astro`. It owns the `/404` route the same way `pages/changelog.astro` takes over the changelog — your page wins and the default is dropped. Build it like any other custom page, in `PageLayout` or `RootLayout`:
280
280
 
@@ -1,7 +1,7 @@
1
1
  import { defineMeta } from "blume";
2
2
 
3
3
  export default defineMeta({
4
- order: 5,
4
+ order: 6,
5
5
  pages: [
6
6
  "skills",
7
7
  "custom-pages",
@@ -0,0 +1,179 @@
1
+ ---
2
+ title: Ask AI
3
+ description: An in-page assistant grounded in your docs — suggested questions, custom instructions, retrieval sizing, backends from the Vercel AI Gateway to any OpenAI-compatible endpoint, and the server output it needs.
4
+ ---
5
+
6
+ Add an assistant that answers reader questions in an in-page chat panel, backed by a streaming server endpoint and the [AI SDK](https://ai-sdk.dev). It's opt-in, and static docs stay fully static until you turn it on:
7
+
8
+ ```ts blume.config.ts lineNumbers
9
+ ai: {
10
+ ask: {
11
+ enabled: true,
12
+ provider: "gateway", // default
13
+ model: "openai/gpt-5.5",
14
+ },
15
+ }
16
+ ```
17
+
18
+ ## Suggested questions
19
+
20
+ Seed the empty state with a few starter prompts. Each renders as a clickable suggestion — click one to send it — with an optional [Lucide icon](/docs/content/components#icon) beside the label:
21
+
22
+ ```ts blume.config.ts lineNumbers
23
+ ai: {
24
+ ask: {
25
+ enabled: true,
26
+ suggestions: [
27
+ { label: "What is Blume?", icon: "rocket" },
28
+ { label: "How do I write a docs page?", icon: "file-text" },
29
+ { label: "How do I configure the theme?", icon: "settings" },
30
+ ],
31
+ },
32
+ }
33
+ ```
34
+
35
+ `label` is the question that gets asked; `icon` is optional. Leave `suggestions` unset (or empty) and the panel opens to a plain input.
36
+
37
+ ## Custom instructions
38
+
39
+ Add your own system-prompt text with `instructions` — identity, language, tone, or anything else the assistant should keep in mind:
40
+
41
+ ```ts blume.config.ts lineNumbers
42
+ ai: {
43
+ ask: {
44
+ enabled: true,
45
+ instructions:
46
+ "You are Bloomy, the Acme docs assistant. Answer in the language the question was asked in, and keep answers under three paragraphs.",
47
+ },
48
+ }
49
+ ```
50
+
51
+ Your text is **appended to** the built-in instructions rather than replacing them: the built-in part carries the [grounding](#grounding) contract — answer only from the retrieved pages, cite them as Markdown links — that the chat panel's citations depend on, so it stays intact whatever you add.
52
+
53
+ ## Grounding
54
+
55
+ Ask AI is **grounded in your docs**. For each question it retrieves the most relevant pages — using the same lexical [Orama](/docs/configuration/search) index that powers on-page search — and injects them into the model's system prompt, so answers come from your content instead of the model's own knowledge. The assistant is told to answer only from the retrieved pages, to say when something isn't covered, and to cite the pages it drew from.
56
+
57
+ The page the reader is currently on is added to the context first and used to scope retrieval to that page's language, so answers stay relevant to where they are in the docs. Retrieval runs at request time from a snapshot baked into the build, so it works regardless of your [search](/docs/configuration/search) provider — even when search is set to `none` — and needs no configuration.
58
+
59
+ Grounding is on for every backend except **[Inkeep](#backends)**, which runs its own retrieval over the content you've indexed in its dashboard.
60
+
61
+ ## Retrieval size
62
+
63
+ How much documentation a question carries is the biggest lever on how long the reader waits for the first word: the model reads every injected character before it emits a token. On a hosted frontier model that's invisible, but on a self-hosted backend it dominates. `retrieval` sizes it:
64
+
65
+ ```ts blume.config.ts lineNumbers
66
+ ai: {
67
+ ask: {
68
+ enabled: true,
69
+ retrieval: {
70
+ maxResults: 3, // fewer pages retrieved per question
71
+ excerptChars: 1200, // shorter excerpt from each one
72
+ contextBudget: 3000, // smaller total injection
73
+ },
74
+ },
75
+ }
76
+ ```
77
+
78
+ | Option | Default | Description |
79
+ | --------------- | ------- | ----------------------------------------------- |
80
+ | `maxResults` | `6` | Documents retrieved per question. |
81
+ | `excerptChars` | `2000` | Characters kept from each retrieved page. |
82
+ | `contextBudget` | `10000` | Total injected characters, across all excerpts. |
83
+
84
+ The three aren't interchangeable. `contextBudget` caps the whole injection, `excerptChars` decides how deep into a single long page its excerpt reaches — raise it when one page holds the whole answer and the excerpt cuts it off — and `maxResults` caps how many pages retrieval adds. The page the reader is viewing is injected on top of the retrieved ones, so an answer can cite up to one page more than `maxResults`.
85
+
86
+ The defaults suit a hosted model. Lower them when you're serving from your own hardware and time-to-first-token matters more than recall; answers stay grounded either way, and the assistant is told to say when something isn't covered rather than fill the gap.
87
+
88
+ ## External endpoint
89
+
90
+ Already have an API backend for AI? Point the panel at it and keep the docs build static:
91
+
92
+ ```ts blume.config.ts lineNumbers
93
+ ai: {
94
+ ask: {
95
+ enabled: true,
96
+ endpoint: "https://api.example.com/v1/docs/ask",
97
+ },
98
+ }
99
+ ```
100
+
101
+ Blume sends the same `POST` body as its built-in route:
102
+
103
+ ```json
104
+ {
105
+ "messages": [{ "role": "user", "content": "How do I deploy?" }],
106
+ "page": { "path": "/deployment" }
107
+ }
108
+ ```
109
+
110
+ Return a successful response whose body is a plain UTF-8 text stream. If the endpoint is on another origin, allow the docs origin with CORS: accept `OPTIONS` and `POST`, permit the `content-type` request header, and return the CORS headers on both the preflight and streamed response. With `endpoint` set, Blume generates the chat UI but no server route, grounding snapshot, provider dependency, or provider-secret warning; your backend owns retrieval, authentication, rate limiting, model access, and citations.
111
+
112
+ ## Server output required
113
+
114
+ Blume's built-in Ask AI backend is a server route (`POST /api/ask`), so it can't run on a static build. Switch to server output and pick an adapter:
115
+
116
+ ```ts blume.config.ts lineNumbers
117
+ deployment: {
118
+ output: "server",
119
+ adapter: "vercel",
120
+ }
121
+ ```
122
+
123
+ A static build with Ask AI enabled and no external `endpoint` fails fast with a message telling you to set `deployment.output` to `server`. See [Deployment](/docs/deployment) for the adapters.
124
+
125
+ ## Backends
126
+
127
+ By default Ask AI routes through the **Vercel AI Gateway**: `model` is a `provider/model` string, so you switch models by changing it (`openai/gpt-5.5`, `anthropic/claude-sonnet-4-5`, and so on) with no provider SDK to install. The gateway reads `AI_GATEWAY_API_KEY` from your environment and is wired up automatically when you deploy on Vercel.
128
+
129
+ Set `provider` to point Ask AI somewhere else. Each backend reads its API key from an environment variable and streams through a provider SDK you install in your project — only the one you use:
130
+
131
+ | `provider` | `model` | API key env var | SDK to install |
132
+ | --- | --- | --- | --- |
133
+ | `gateway` (default) | a `provider/model` string via the AI Gateway | `AI_GATEWAY_API_KEY` | none — ships with Blume |
134
+ | `openrouter` | any [OpenRouter](https://openrouter.ai) model | `OPENROUTER_API_KEY` | `@openrouter/ai-sdk-provider` |
135
+ | `llmgateway` | any [LLMGateway](https://llmgateway.io) model | `LLMGATEWAY_API_KEY` | `@ai-sdk/openai-compatible` |
136
+ | `inkeep` | an [Inkeep](https://inkeep.com) QA model | `INKEEP_API_KEY` | `@ai-sdk/openai-compatible` |
137
+ | `openai-compatible` | whatever your endpoint serves | set with `apiKeyEnv` | `@ai-sdk/openai-compatible` |
138
+
139
+ The SDKs are optional peer dependencies, so add the one your backend needs to your project (e.g. `npm install @openrouter/ai-sdk-provider`). If it's missing, the build warns with the exact package name before Vite would fail to resolve the import.
140
+
141
+ For example, to use OpenRouter:
142
+
143
+ ```ts blume.config.ts lineNumbers
144
+ ai: {
145
+ ask: {
146
+ enabled: true,
147
+ provider: "openrouter",
148
+ model: "anthropic/claude-sonnet-4-5",
149
+ },
150
+ }
151
+ ```
152
+
153
+ Any OpenAI-compatible endpoint works through `openai-compatible` — supply the `baseUrl` and the env var holding its key:
154
+
155
+ ```ts blume.config.ts lineNumbers
156
+ ai: {
157
+ ask: {
158
+ enabled: true,
159
+ provider: "openai-compatible",
160
+ baseUrl: "https://my-gateway.example.com/v1",
161
+ apiKeyEnv: "MY_GATEWAY_API_KEY",
162
+ model: "gpt-4o",
163
+ },
164
+ }
165
+ ```
166
+
167
+ Set `apiKeyEnv` (and, for the named providers, `baseUrl`) on any backend to point at a different env var or proxy.
168
+
169
+ :::note
170
+ **Inkeep** answers from the content you've indexed in the Inkeep dashboard — it runs its own retrieval — so Blume leaves it ungrounded. Every other backend is [grounded](#grounding) in this site's pages.
171
+ :::
172
+
173
+ Keys are read with `process.env`, which covers the Node, Vercel, and Netlify adapters. On Cloudflare, expose the key through the platform's [runtime binding](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets). Enabling Ask AI also turns on React for the in-page island — see [Customization](/docs/configuration/customization#interactive-islands).
174
+
175
+ ## Rate limiting
176
+
177
+ The `POST /api/ask` endpoint is **unauthenticated** — it has to be, so the in-page assistant can call it. Blume validates each request — rejecting malformed bodies, capping it to 1–40 messages, and accepting only `user`/`assistant` roles so a caller can't inject their own system prompt and repurpose the route as a general LLM proxy — to bound how much a single call can spend against your model, but it can't stop someone from calling the endpoint repeatedly. If cost abuse is a concern, put the route behind a rate limiter — your host's (e.g. Vercel's) edge rate limiting, a middleware, or your model provider's per-key spend limits.
178
+
179
+ The endpoint is advertised in the [agent readability manifest](/docs/discoverability/agent-discovery#agent-readability) alongside the rest of the site's machine-readable surface.
@@ -65,7 +65,7 @@ export default defineConfig({
65
65
  },
66
66
  },
67
67
 
68
- // AI — see the AI guide
68
+ // AI — llms.txt, MCP; see the Discoverability section
69
69
  ai: {
70
70
  llmsTxt: true,
71
71
  // MCP server (needs server output)
@@ -75,7 +75,7 @@ export default defineConfig({
75
75
  },
76
76
  },
77
77
 
78
- // SEO — OG images, feeds, sitemap, structured data; see the SEO guide
78
+ // SEO — OG images, feeds, sitemap, structured data; see the Discoverability section
79
79
  seo: {
80
80
  og: { enabled: true },
81
81
  rss: { enabled: true, types: ["blog", "changelog"] },
@@ -266,7 +266,7 @@ export default defineConfig({
266
266
 
267
267
  A key can be declared site-wide or per-type, not both. See [Per-type keys](/docs/reference/frontmatter#per-type-keys) for how the scoping resolves.
268
268
 
269
- `facets` names the custom keys whose values become filterable metadata: they ride along on search documents (`blume-search.json` and the MCP index), and the [MCP tools](/docs/configuration/ai#mcp-server) accept a `filters` input matching against them, so an agent can retrieve, say, only `enforced` RFCs in the `architecture` domain. Each facet must be a declared custom key — per-type or site-wide — and only string (or stringified number/boolean) values facet.
269
+ `facets` names the custom keys whose values become filterable metadata: they ride along on search documents (`blume-search.json` and the MCP index), and the [MCP tools](/docs/discoverability/mcp) accept a `filters` input matching against them, so an agent can retrieve, say, only `enforced` RFCs in the `architecture` domain. Each facet must be a declared custom key — per-type or site-wide — and only string (or stringified number/boolean) values facet.
270
270
 
271
271
  ## GitHub
272
272
 
@@ -363,9 +363,9 @@ dateFormat: { year: "numeric", month: "2-digit", day: "2-digit" },
363
363
  | `timeZone` | IANA time zone. Defaults to `UTC`, so a date reads the same regardless of where the site builds. |
364
364
  | `calendar`, `numberingSystem` | Calendar system (e.g. `"japanese"`) and numbering system (e.g. `"arab"`). |
365
365
 
366
- ## SEO
366
+ ## SEO and AI
367
367
 
368
- Open Graph images, RSS feeds, and JSON-LD structured data, grouped under `seo`. See the [SEO guide](/docs/configuration/seo) for metadata, frontmatter overrides, and the full reference.
368
+ Metadata, Open Graph images, RSS feeds, JSON-LD, the sitemap, and `robots.txt` live under `seo`; `llms.txt`, raw Markdown, the JSON API, and the MCP server live under `ai`. Both are covered page by page in the [Discoverability](/docs/discoverability) section, which treats search engines and AI agents as two audiences for the same machine-readable layer.
369
369
 
370
370
  ```ts blume.config.ts lineNumbers
371
371
  seo: {
@@ -417,9 +417,10 @@ Each of these has its own guide. The config field is the entry point:
417
417
  | `navigation` | Explicit sidebar and header tabs | [Navigation](/docs/content/navigation) |
418
418
  | `search` | Provider (Orama, Pagefind, Algolia, and more) and indexing | [Search](/docs/configuration/search) |
419
419
  | `markdown` | Markdown rendering options — code blocks, heading anchors, image zoom | [Syntax](/docs/content/syntax) |
420
- | `ai` | `llms.txt`, Ask AI, and the hosted MCP server for coding agents | [AI](/docs/configuration/ai) |
420
+ | `ai` | `llms.txt`, Markdown mirrors, the JSON API, and the hosted MCP server for coding agents | [SEO and AEO](/docs/discoverability) |
421
+ | `ai.ask` | The in-page Ask AI assistant | [Ask AI](/docs/configuration/ask-ai) |
421
422
  | `analytics` | Vercel, PostHog, and custom scripts | [Analytics](/docs/configuration/analytics) |
422
- | `seo` | Metadata, OG images, feeds, structured data | [SEO](/docs/configuration/seo) |
423
+ | `seo` | Metadata, OG images, feeds, structured data, sitemap, robots | [SEO and AEO](/docs/discoverability) |
423
424
  | `deployment` | Output mode, adapter, and site URL | [Deployment](/docs/deployment) |
424
425
  | `redirects` | Permanent and temporary redirects | [Deployment](/docs/deployment#redirects) |
425
426
  | `integrations` | Astro integrations appended after Blume's built-ins | [Customization](/docs/configuration/customization#astro-integrations) |
@@ -6,10 +6,9 @@ export default defineMeta({
6
6
  "theming",
7
7
  "customization",
8
8
  "search",
9
- "ai",
9
+ "ask-ai",
10
10
  "analytics",
11
11
  "export",
12
- "seo",
13
12
  ],
14
13
  title: "Configuration",
15
14
  });
@@ -52,7 +52,7 @@ search: {
52
52
  },
53
53
  ```
54
54
 
55
- Each fence's body and title (`blume.config.ts` above) become searchable; the language and fence markers don't. On `.mdx` pages the index reads components as the text they show — a Card's title, a Tab's label, a TypeTable's descriptions — using the same serializers as the [agent surfaces](/docs/configuration/ai), so an `ai.markdownComponents` entry covers your own components too. The option has no effect on Pagefind or Mixedbread. Expect the index to grow with your fenced content — the client index ships to every reader, hosted providers cap record size (Algolia rejects the sync batch when one page's record exceeds its plan's limit, leaving the previous index live), and a hit inside a fence shows flattened code in the result excerpt.
55
+ Each fence's body and title (`blume.config.ts` above) become searchable; the language and fence markers don't. On `.mdx` pages the index reads components as the text they show — a Card's title, a Tab's label, a TypeTable's descriptions — using the same serializers as the [agent surfaces](/docs/discoverability/markdown), so an `ai.markdownComponents` entry covers your own components too. The option has no effect on Pagefind or Mixedbread. Expect the index to grow with your fenced content — the client index ships to every reader, hosted providers cap record size (Algolia rejects the sync batch when one page's record exceeds its plan's limit, leaving the previous index live), and a hit inside a fence shows flattened code in the result excerpt.
56
56
 
57
57
  On a [versioned](/docs/content/versioning) site, results default to the version being viewed, with an "All versions" toggle in the dialog footer (remembered per reader). Cross-version hits name their version on the row. Orama, FlexSearch, Algolia, and Typesense honor the scoping — hosted records carry a `version` facet, with the current docs uploaded as `"current"` — while Pagefind stays unscoped, matching its locale behavior.
58
58
 
@@ -145,7 +145,7 @@ theme: {
145
145
  Paths resolve from the project root. `weight` and `style` (`normal`, `italic`, `oblique`) are optional — Astro reads them from the font file when omitted.
146
146
 
147
147
  :::note
148
- When you set `theme.fonts` explicitly, your display and body fonts also style the generated [Open Graph cards](/docs/configuration/seo#card-fonts) automatically, so shared links match the site. Non-Google provider families are skipped there (the card renderer can only fetch from Google Fonts); local files work everywhere.
148
+ When you set `theme.fonts` explicitly, your display and body fonts also style the generated [Open Graph cards](/docs/discoverability/open-graph#card-fonts) automatically, so shared links match the site. Non-Google provider families are skipped there (the card renderer can only fetch from Google Fonts); local files work everywhere.
149
149
  :::
150
150
 
151
151
  Want to drop back to the system stack? Override the `--blume-font-*` tokens directly in [`theme.css`](#themecss).
@@ -251,7 +251,7 @@ Lay cards or blocks out in a responsive grid of equal columns that reflows on mo
251
251
 
252
252
  ## CodeGroup
253
253
 
254
- Group several code blocks into one tabbed switcher — a tab per language or file. The tab label is each block's title (the text after the language). Add `dropdown` to switch with a menu instead of a tab bar.
254
+ Group several code blocks into one tabbed switcher — a tab per language or file. The tab label is each block's title (the text after the language), and the group's copy button sits in the tab bar and copies the block that is showing. Add `dropdown` to switch with a menu instead of a tab bar.
255
255
 
256
256
  <CodeGroup>
257
257
 
@@ -102,6 +102,12 @@ Fallback pages are excluded from the search index and aren't advertised as real
102
102
  Start by translating your most important pages — the homepage, quickstart, and top guides — and let the rest fall back. You can fill in translations over time without breaking any links.
103
103
  :::
104
104
 
105
+ ## Links across locales
106
+
107
+ Write internal links the way you would in the default locale — `[Setup](/guides/setup)`, `<Card href="/guides/setup">` — in every language, including translated pages. When a page renders under a locale prefix, Blume moves each root-relative page link into that locale (`/fr/guides/setup`) as long as the route is served there, as a real translation or as a fallback page. A link with no per-locale variant — a custom page, a generated route, or a missing translation on a site with fallbacks disabled — keeps its authored target rather than pointing at a 404, and a link that already carries a locale prefix (`/de/guides/setup`) is left alone, so cross-locale links stay explicit.
108
+
109
+ Anchors travel with the link, so heading ids have to agree across languages. [`blume translate`](/docs/reference/translate) takes care of that: every translated heading is pinned to its source heading's id with a trailing `[#id]` marker. In a translation you write by hand, pin the headings yourself with the same [`[#custom-id]` marker](/docs/content/syntax#custom-anchors) — otherwise `#ordering` won't match the French page's auto-generated `#ordre`, and `blume validate` reports the mismatch against the translated page a reader actually lands on.
110
+
105
111
  ## Translating with an agent
106
112
 
107
113
  You don't have to fill in the locales by hand. [`blume translate`](/docs/reference/translate) finds every page that's missing or outdated in each locale and translates it with a local agent CLI ([Claude Code](https://claude.com/claude-code) or [Codex](https://developers.openai.com/codex/cli)):
@@ -160,7 +166,7 @@ Set `dir: "rtl"` on a locale and Blume mirrors the whole interface — the sideb
160
166
  <Card title="Navigation" href="/docs/content/navigation" icon="menu">
161
167
  Shape each locale's sidebar, ordering, and tabs.
162
168
  </Card>
163
- <Card title="SEO" href="/docs/configuration/seo" icon="rocket">
169
+ <Card title="Discoverability" href="/docs/discoverability" icon="rocket">
164
170
  Sitemaps, Open Graph, and structured data.
165
171
  </Card>
166
172
  </CardGroup>
@@ -82,7 +82,7 @@ The type is independent of where the file lives, but by convention blog posts go
82
82
 
83
83
  ## Feeds
84
84
 
85
- Blume generates an RSS feed automatically for each content type listed in [`rss.types`](/docs/configuration/seo#rss-feeds) — `blog` and `changelog` by default — as long as it has at least one page. Feeds are served at `/<type>/rss.xml`:
85
+ Blume generates an RSS feed automatically for each content type listed in [`rss.types`](/docs/discoverability/rss) — `blog` and `changelog` by default — as long as it has at least one page. Feeds are served at `/<type>/rss.xml`:
86
86
 
87
87
  | Type | Feed |
88
88
  | ----------- | -------------------- |
@@ -275,7 +275,7 @@ navigation: {
275
275
  }
276
276
  ```
277
277
 
278
- That's for a project whose docs repo is private. `github` drives the per-page edit link, the header mark and the [agent manifest](/docs/configuration/ai)'s repository together, so such a project has to leave `github` unset — and a URL is what lets it still show a mark pointing somewhere public. The icon stays the GitHub mark, so a link to another host belongs in [`actions`](#header-actions).
278
+ That's for a project whose docs repo is private. `github` drives the per-page edit link, the header mark and the [agent manifest](/docs/discoverability/agent-discovery)'s repository together, so such a project has to leave `github` unset — and a URL is what lets it still show a mark pointing somewhere public. The icon stays the GitHub mark, so a link to another host belongs in [`actions`](#header-actions).
279
279
 
280
280
  ## Breadcrumbs and pagination
281
281
 
@@ -296,6 +296,6 @@ Below the table of contents, every page shows a set of quick actions:
296
296
  - **Scroll to top** — smoothly returns to the top of long pages.
297
297
  - **Give feedback** — opens a prefilled GitHub issue with an optional reaction and note (also requires `github`).
298
298
 
299
- Others hand the page to AI tools — **Copy as Markdown** and **Open in chat** — covered in [AI](/docs/configuration/ai#copy-as-markdown).
299
+ Others hand the page to AI tools — **Copy as Markdown** and **Open in chat** — covered in [AI](/docs/discoverability/markdown#copy-as-markdown).
300
300
 
301
301
  With [`export`](/docs/configuration/export) on, an **Export** action also lets readers download the page as a PDF or EPUB.
@@ -359,7 +359,7 @@ Hide the language icons or wrap long lines instead of scrolling with `markdown:
359
359
 
360
360
  ## Package install
361
361
 
362
- A `package-install` block turns a single install command into a tabbed snippet for npm, pnpm, yarn, and bun — so readers copy the one that matches their setup. Like diagrams and math, this is an MDX-only feature — in a `.md` file the block renders as a plain code fence.
362
+ A `package-install` block turns a single install command into a tabbed snippet for npm, pnpm, yarn, bun, nub, and aube — so readers copy the one that matches their setup. Like diagrams and math, this is an MDX-only feature — in a `.md` file the block renders as a plain code fence.
363
363
 
364
364
  ```package-install
365
365
  npm i blume