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.
- package/CHANGELOG.md +25 -0
- package/dist/cli/index.js +307 -68
- package/dist/cli/index.js.map +28 -23
- package/dist/types/ai/component-markdown.d.ts +14 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/02-deployment.mdx +5 -5
- package/docs/{07-faq.mdx → 08-faq.mdx} +7 -7
- package/docs/advanced/blog.mdx +3 -3
- package/docs/advanced/changelog.mdx +2 -2
- package/docs/advanced/custom-pages.mdx +4 -4
- package/docs/advanced/meta.ts +1 -1
- package/docs/configuration/ask-ai.mdx +179 -0
- package/docs/configuration/index.mdx +8 -7
- package/docs/configuration/meta.ts +1 -2
- package/docs/configuration/search.mdx +1 -1
- package/docs/configuration/theming.mdx +1 -1
- package/docs/content/components.mdx +1 -1
- package/docs/content/i18n.mdx +7 -1
- package/docs/content/index.mdx +1 -1
- package/docs/content/navigation.mdx +2 -2
- package/docs/content/syntax.mdx +1 -1
- package/docs/discoverability/agent-discovery.mdx +196 -0
- package/docs/discoverability/index.mdx +48 -0
- package/docs/discoverability/json-api.mdx +58 -0
- package/docs/discoverability/llms-txt.mdx +68 -0
- package/docs/discoverability/markdown.mdx +76 -0
- package/docs/discoverability/mcp.mdx +64 -0
- package/docs/discoverability/meta.ts +18 -0
- package/docs/discoverability/metadata.mdx +82 -0
- package/docs/discoverability/open-graph.mdx +113 -0
- package/docs/discoverability/rss.mdx +24 -0
- package/docs/discoverability/sitemap-and-robots.mdx +95 -0
- package/docs/discoverability/structured-data.mdx +51 -0
- package/docs/index.mdx +5 -5
- package/docs/reference/eval.mdx +1 -1
- package/docs/reference/meta.ts +1 -1
- package/docs/reference/translate.mdx +1 -0
- package/package.json +18 -18
- package/src/ai/component-markdown.ts +17 -2
- package/src/ai/llms.ts +3 -10
- package/src/ai/markdown.ts +3 -10
- package/src/ai/openapi-components.ts +123 -0
- package/src/ai/serializers.ts +24 -0
- package/src/astro/templates.ts +42 -12
- package/src/audit/checks/links.ts +1 -8
- package/src/audit/checks/llms.ts +5 -4
- package/src/audit/redirects.ts +4 -3
- package/src/audit/run.ts +6 -8
- package/src/audit/url.ts +33 -0
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/Component.astro +65 -68
- package/src/components/content/Tabs.astro +24 -9
- package/src/components/content/example-pane.ts +6 -0
- package/src/components/layout/LocaleLinks.astro +42 -0
- package/src/components/layout/PageLayout.astro +5 -3
- package/src/components/layout/ReferenceLayout.astro +5 -0
- package/src/components/layout/RootLayout.astro +110 -39
- package/src/components/layout/search-locale.ts +13 -0
- package/src/components/openapi/ApiOverview.astro +7 -39
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/AsyncApiOperation.astro +3 -2
- package/src/components/openapi/GraphqlOperation.astro +3 -2
- package/src/components/openapi/Operation.astro +3 -2
- package/src/core/i18n.ts +13 -2
- package/src/core/links.ts +33 -1
- package/src/core/locale-links.ts +163 -0
- package/src/core/sources/normalize.ts +57 -7
- package/src/markdown/package-commands.ts +27 -3
- package/src/openapi/graphql.ts +29 -0
- package/src/openapi/model.ts +69 -0
- package/src/openapi/render-mdx.ts +3 -2
- package/src/openapi/signature.ts +18 -0
- package/src/search/documents.ts +4 -9
- package/src/theme/code-block-padding.ts +0 -8
- package/src/theme/entry.ts +33 -27
- package/src/translate/anchors.ts +91 -0
- package/src/translate/validate.ts +8 -3
- package/docs/configuration/ai.mdx +0 -613
- 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
|
package/docs/01-quickstart.mdx
CHANGED
|
@@ -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="
|
|
90
|
-
Ship `llms.txt
|
|
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.
|
package/docs/02-deployment.mdx
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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.
|
|
90
|
+
1. Save the patch as `patches/oxfmt@0.67.0.patch`:
|
|
91
91
|
|
|
92
|
-
```diff patches/oxfmt@0.
|
|
93
|
-
diff --git a/dist/markdown-
|
|
94
|
-
index
|
|
95
|
-
--- a/dist/markdown-
|
|
96
|
-
+++ b/dist/markdown-
|
|
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.
|
|
149
|
+
"oxfmt@0.67.0": "patches/oxfmt@0.67.0.patch"
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
```
|
package/docs/advanced/blog.mdx
CHANGED
|
@@ -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/
|
|
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/
|
|
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="
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
package/docs/advanced/meta.ts
CHANGED
|
@@ -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
|
|
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
|
|
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/
|
|
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,
|
|
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`,
|
|
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/
|
|
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) |
|
|
@@ -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/
|
|
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/
|
|
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
|
|
package/docs/content/i18n.mdx
CHANGED
|
@@ -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="
|
|
169
|
+
<Card title="Discoverability" href="/docs/discoverability" icon="rocket">
|
|
164
170
|
Sitemaps, Open Graph, and structured data.
|
|
165
171
|
</Card>
|
|
166
172
|
</CardGroup>
|
package/docs/content/index.mdx
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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.
|
package/docs/content/syntax.mdx
CHANGED
|
@@ -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
|
|
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
|