blume 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/dist/cli/index.js +1286 -63
- package/dist/cli/index.js.map +32 -21
- package/dist/types/core/config-input.d.ts +18 -0
- package/dist/types/core/config.d.ts +4 -0
- package/dist/types/core/data.d.ts +1 -0
- package/dist/types/core/schema.d.ts +132 -17
- package/dist/types/core/types.d.ts +5 -3
- package/dist/types/openapi/references.d.ts +6 -0
- package/docs/advanced/api-reference.mdx +27 -0
- package/docs/advanced/changelog.mdx +10 -0
- package/docs/configuration/ai.mdx +38 -2
- package/docs/configuration/customization.mdx +27 -0
- package/docs/configuration/index.mdx +5 -0
- package/docs/content/navigation.mdx +12 -0
- package/docs/reference/cli.mdx +17 -13
- package/docs/reference/eval.mdx +106 -0
- package/docs/reference/meta.ts +1 -1
- package/package.json +1 -1
- package/src/ai/agent-readability.ts +19 -1
- package/src/ai/llms.ts +9 -4
- package/src/ai/mcp/server.ts +19 -8
- package/src/ai/mcp/stdio.ts +35 -0
- package/src/astro/generate.ts +25 -2
- package/src/astro/templates.ts +114 -22
- package/src/cli/commands/eval.ts +291 -0
- package/src/cli/commands/init.ts +9 -4
- package/src/cli/commands/mcp-stdio.ts +36 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/required-secrets.ts +1 -1
- package/src/components/content/AccordionItem.astro +2 -2
- package/src/components/content/TreeFolder.astro +1 -2
- package/src/components/islands/AskAI.astro +9 -2
- package/src/components/islands/ask-ai.tsx +4 -2
- package/src/components/islands/hooks.ts +10 -4
- package/src/components/layout/NavTree.astro +37 -19
- package/src/components/layout/ReferenceLayout.astro +4 -0
- package/src/components/layout/RootLayout.astro +1 -1
- package/src/components/openapi/SchemaProperty.astro +3 -3
- package/src/core/config-input.ts +18 -0
- package/src/core/config.ts +4 -0
- package/src/core/data.ts +1 -0
- package/src/core/graph.ts +1 -0
- package/src/core/navigation.ts +9 -2
- package/src/core/schema.ts +51 -4
- package/src/core/server-features.ts +1 -1
- package/src/core/types.ts +5 -3
- package/src/eval/agents.ts +340 -0
- package/src/eval/findings.ts +103 -0
- package/src/eval/prompts.ts +78 -0
- package/src/eval/report.ts +214 -0
- package/src/eval/run.ts +290 -0
- package/src/eval/schema.ts +124 -0
- package/src/openapi/references.ts +23 -2
- package/src/openapi/render-mdx.ts +27 -4
- package/src/openapi/scalar.ts +1 -0
- package/src/openapi/source.ts +11 -4
- package/src/registry/eject.ts +23 -1
- package/src/search/build.ts +4 -3
|
@@ -65,7 +65,7 @@ const nextId = (): number => {
|
|
|
65
65
|
|
|
66
66
|
// The endpoint and page path both honor the deployment `base` so grounding works
|
|
67
67
|
// under a non-root base path (the server matches base-less document routes).
|
|
68
|
-
const
|
|
68
|
+
const DEFAULT_ASK_ENDPOINT = joinBase(import.meta.env.BASE_URL, "api/ask");
|
|
69
69
|
|
|
70
70
|
/** The current route with the deployment base stripped, for page-context lookup. */
|
|
71
71
|
const currentPath = (): string =>
|
|
@@ -133,10 +133,12 @@ const ANSWER_CLASS =
|
|
|
133
133
|
"prose prose-sm max-w-none text-foreground [&_a]:inline-flex [&_a]:items-center [&_a]:gap-1 [&_a]:rounded-full [&_a]:bg-muted [&_a]:px-2 [&_a]:py-1 [&_a]:align-middle [&_a]:font-medium [&_a]:text-[0.7rem] [&_a]:leading-none [&_a]:text-muted-foreground! [&_a]:no-underline! [&_a:hover]:text-foreground!";
|
|
134
134
|
|
|
135
135
|
const AskAI = ({
|
|
136
|
+
endpoint = DEFAULT_ASK_ENDPOINT,
|
|
136
137
|
icons = EMPTY_ICONS,
|
|
137
138
|
strings,
|
|
138
139
|
suggestions = EMPTY_SUGGESTIONS,
|
|
139
140
|
}: {
|
|
141
|
+
endpoint?: string;
|
|
140
142
|
icons?: AskIcons;
|
|
141
143
|
strings?: UIStrings["ask"];
|
|
142
144
|
suggestions?: Suggestion[];
|
|
@@ -268,7 +270,7 @@ const AskAI = ({
|
|
|
268
270
|
abortRef.current = controller;
|
|
269
271
|
|
|
270
272
|
try {
|
|
271
|
-
const response = await fetch(
|
|
273
|
+
const response = await fetch(endpoint, {
|
|
272
274
|
body: JSON.stringify({
|
|
273
275
|
messages: history.map((m) => ({ content: m.content, role: m.role })),
|
|
274
276
|
page: { path: currentPath() },
|
|
@@ -139,7 +139,12 @@ export interface UseAskAI {
|
|
|
139
139
|
reset: () => void;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
const
|
|
142
|
+
const DEFAULT_ASK_ENDPOINT = joinBase(import.meta.env.BASE_URL, "api/ask");
|
|
143
|
+
|
|
144
|
+
export interface UseAskAIOptions {
|
|
145
|
+
/** Existing Ask AI endpoint; defaults to Blume's generated `/api/ask`. */
|
|
146
|
+
endpoint?: string;
|
|
147
|
+
}
|
|
143
148
|
|
|
144
149
|
/** Shown as the assistant's answer when the request fails or throws. */
|
|
145
150
|
const ASK_ERROR = "Something went wrong answering that. Please try again.";
|
|
@@ -152,7 +157,8 @@ const currentPath = (): string =>
|
|
|
152
157
|
* Stream answers from the Ask AI endpoint. Mirrors the built-in Ask AI island so
|
|
153
158
|
* a custom chat UI shares the same grounded, page-aware backend.
|
|
154
159
|
*/
|
|
155
|
-
export const useAskAI = (): UseAskAI => {
|
|
160
|
+
export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
|
|
161
|
+
const endpoint = options.endpoint ?? DEFAULT_ASK_ENDPOINT;
|
|
156
162
|
const [messages, setMessages] = useState<AskMessage[]>([]);
|
|
157
163
|
const [loading, setLoading] = useState(false);
|
|
158
164
|
// The stream writes into the conversation via state updates, so `reset()`
|
|
@@ -186,7 +192,7 @@ export const useAskAI = (): UseAskAI => {
|
|
|
186
192
|
setMessages([...history, assistant]);
|
|
187
193
|
setLoading(true);
|
|
188
194
|
try {
|
|
189
|
-
const response = await fetch(
|
|
195
|
+
const response = await fetch(endpoint, {
|
|
190
196
|
body: JSON.stringify({
|
|
191
197
|
messages: history,
|
|
192
198
|
page: { path: currentPath() },
|
|
@@ -239,7 +245,7 @@ export const useAskAI = (): UseAskAI => {
|
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
},
|
|
242
|
-
[loading, messages]
|
|
248
|
+
[endpoint, loading, messages]
|
|
243
249
|
);
|
|
244
250
|
|
|
245
251
|
// Retained for the compiler-off opt-out path (`react: { compiler: false }`):
|
|
@@ -129,29 +129,43 @@ const initialId =
|
|
|
129
129
|
</div>
|
|
130
130
|
{panels.map((panel) => (
|
|
131
131
|
<div data-nav-panel={panel.id} hidden={panel.id !== initialId}>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
{/* The title is the only sidebar link to the section's own page, so
|
|
133
|
+
a routed panel keeps it as a link; without a route the whole row
|
|
134
|
+
becomes the back button. Either way every part of the row is
|
|
135
|
+
interactive. */}
|
|
136
|
+
{panel.route ? (
|
|
137
|
+
<div class="mb-3 flex items-center gap-0.5">
|
|
138
|
+
<button
|
|
139
|
+
aria-label={n.back}
|
|
140
|
+
class="-ml-1 flex shrink-0 items-center justify-center self-stretch rounded px-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
141
|
+
data-nav-back={panel.parentId}
|
|
142
|
+
type="button"
|
|
143
|
+
>
|
|
144
|
+
<Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
|
|
145
|
+
</button>
|
|
142
146
|
<a
|
|
143
147
|
aria-current={panel.route === currentRoute ? "page" : undefined}
|
|
144
|
-
class="flex-1 truncate font-semibold text-foreground text-sm hover:
|
|
148
|
+
class="flex-1 truncate rounded px-1 py-1 font-semibold text-foreground text-sm transition-colors hover:bg-muted"
|
|
145
149
|
href={withBase(panel.route)}
|
|
146
150
|
>
|
|
147
151
|
{panel.label}
|
|
148
152
|
</a>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
</div>
|
|
154
|
+
) : (
|
|
155
|
+
<button
|
|
156
|
+
aria-label={`${n.back}: ${panel.label}`}
|
|
157
|
+
class="-ml-1 mb-3 flex w-full items-center gap-1.5 rounded p-1 text-left font-semibold text-foreground text-sm transition-colors hover:bg-muted"
|
|
158
|
+
data-nav-back={panel.parentId}
|
|
159
|
+
type="button"
|
|
160
|
+
>
|
|
161
|
+
<Icon
|
|
162
|
+
class="shrink-0 text-muted-foreground rtl:-scale-x-100"
|
|
163
|
+
name="arrow-left"
|
|
164
|
+
size={16}
|
|
165
|
+
/>
|
|
166
|
+
<span class="flex-1 truncate">{panel.label}</span>
|
|
167
|
+
</button>
|
|
168
|
+
)}
|
|
155
169
|
<Self
|
|
156
170
|
currentRoute={currentRoute}
|
|
157
171
|
depth={1}
|
|
@@ -236,7 +250,7 @@ const initialId =
|
|
|
236
250
|
const open = active || item.collapsed === false;
|
|
237
251
|
return (
|
|
238
252
|
<li class={spacing}>
|
|
239
|
-
<details
|
|
253
|
+
<details open={open}>
|
|
240
254
|
<summary class="flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden">
|
|
241
255
|
{item.route ? (
|
|
242
256
|
<a
|
|
@@ -273,7 +287,11 @@ const initialId =
|
|
|
273
287
|
)}
|
|
274
288
|
</>
|
|
275
289
|
)}
|
|
276
|
-
|
|
290
|
+
{/* Scope the rotation to this group's own `details` — the
|
|
291
|
+
`group-open` variant matches any open ancestor `.group`,
|
|
292
|
+
so nested chevrons rotated while their own group stayed
|
|
293
|
+
closed. */}
|
|
294
|
+
<span class="shrink-0 text-muted-foreground transition-transform [details[open]>summary_&]:rotate-90">
|
|
277
295
|
<Icon name="chevron-right" size={13} />
|
|
278
296
|
</span>
|
|
279
297
|
</summary>
|
|
@@ -53,6 +53,8 @@ interface Props {
|
|
|
53
53
|
fontCssVars?: string[];
|
|
54
54
|
searchEnabled: boolean;
|
|
55
55
|
pageTitle: string;
|
|
56
|
+
/** Keep the reference route out of crawler indexes. */
|
|
57
|
+
noindex?: boolean;
|
|
56
58
|
/** Active locale code for `<html lang>` (defaults to `en`). */
|
|
57
59
|
locale?: string;
|
|
58
60
|
/** Text direction for `<html dir>` (defaults to `ltr`). */
|
|
@@ -74,6 +76,7 @@ const {
|
|
|
74
76
|
fontCssVars,
|
|
75
77
|
searchEnabled,
|
|
76
78
|
pageTitle,
|
|
79
|
+
noindex = false,
|
|
77
80
|
locale = "en",
|
|
78
81
|
dir = "ltr",
|
|
79
82
|
ui,
|
|
@@ -89,6 +92,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
89
92
|
<head>
|
|
90
93
|
<meta charset="utf-8" />
|
|
91
94
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
95
|
+
{noindex && <meta name="robots" content="noindex" />}
|
|
92
96
|
<title>{pageTitle}</title>
|
|
93
97
|
<Favicon favicon={favicon} appleIcon={appleIcon} />
|
|
94
98
|
<Fonts cssVars={fontCssVars ?? []} />
|
|
@@ -359,7 +359,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
359
359
|
---
|
|
360
360
|
|
|
361
361
|
<!doctype html>
|
|
362
|
-
<html dir={dir} lang={locale}>
|
|
362
|
+
<html data-pagefind-ignore={indexable ? undefined : "all"} dir={dir} lang={locale}>
|
|
363
363
|
<head>
|
|
364
364
|
<meta charset="utf-8" />
|
|
365
365
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
@@ -99,10 +99,10 @@ const expandable =
|
|
|
99
99
|
}
|
|
100
100
|
{
|
|
101
101
|
expandable && (
|
|
102
|
-
<details class="
|
|
102
|
+
<details class="mt-2" open={expandAll}>
|
|
103
103
|
<summary class="cursor-pointer select-none text-accent text-xs hover:underline">
|
|
104
|
-
<span class="
|
|
105
|
-
<span class="hidden
|
|
104
|
+
<span class="[details[open]>summary_&]:hidden">Show properties</span>
|
|
105
|
+
<span class="hidden [details[open]>summary_&]:inline">Hide properties</span>
|
|
106
106
|
</summary>
|
|
107
107
|
<div class="mt-2 border-border border-l pl-4">
|
|
108
108
|
<SchemaTable
|
package/src/core/config-input.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AstroIntegration } from "astro";
|
|
1
2
|
import type { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import type { ComponentMarkdown } from "../ai/component-markdown.ts";
|
|
@@ -290,6 +291,15 @@ export interface NavTabItem {
|
|
|
290
291
|
|
|
291
292
|
/** A top-level tab in the header, optionally opening a dropdown of items. */
|
|
292
293
|
export interface NavTab {
|
|
294
|
+
/**
|
|
295
|
+
* Where the tab links to, when that differs from `path`. `path` scopes the
|
|
296
|
+
* sidebar section and matches the active tab; without `href`, a section whose
|
|
297
|
+
* `path` isn't itself a page falls back to the section's first page, or keeps
|
|
298
|
+
* `path` when the section has no linkable page at all. Set this to send
|
|
299
|
+
* readers somewhere else — e.g. a generated `/changelog` index, or a custom
|
|
300
|
+
* `.astro` landing page, neither of which is part of the content tree.
|
|
301
|
+
*/
|
|
302
|
+
href?: string;
|
|
293
303
|
/** Lucide icon name shown beside the label. */
|
|
294
304
|
icon?: string;
|
|
295
305
|
/** Dropdown items; omit for a plain link tab. */
|
|
@@ -522,6 +532,12 @@ export interface AskConfig {
|
|
|
522
532
|
baseUrl?: string;
|
|
523
533
|
/** Turn Ask AI on. Defaults to `false`. */
|
|
524
534
|
enabled?: boolean;
|
|
535
|
+
/**
|
|
536
|
+
* Existing Ask AI endpoint to call instead of generating one. This keeps a
|
|
537
|
+
* Blume site static while an API backend owns retrieval, model access, rate
|
|
538
|
+
* limiting, and streaming. Accepts an absolute URL or root-relative path.
|
|
539
|
+
*/
|
|
540
|
+
endpoint?: string;
|
|
525
541
|
/** Model id to use. Defaults to `openai/gpt-5.5`. */
|
|
526
542
|
model?: string;
|
|
527
543
|
/** Which backend routes the request. Defaults to `gateway`. */
|
|
@@ -1115,6 +1131,8 @@ export interface BlumeConfig {
|
|
|
1115
1131
|
github?: GithubConfig;
|
|
1116
1132
|
/** Internationalization (opt-in multi-locale). */
|
|
1117
1133
|
i18n?: I18nConfig;
|
|
1134
|
+
/** Astro integrations appended after Blume's built-ins, in declaration order. */
|
|
1135
|
+
integrations?: AstroIntegration[];
|
|
1118
1136
|
/** "Last updated" timestamps from git history or frontmatter. Defaults to `false`. */
|
|
1119
1137
|
lastModified?: LastModifiedConfig;
|
|
1120
1138
|
/** Site logo / brand mark. */
|
package/src/core/config.ts
CHANGED
|
@@ -84,6 +84,10 @@ import type { Diagnostic } from "./types.ts";
|
|
|
84
84
|
* - `analytics` — PostHog, Vercel, or arbitrary `scripts` (Plausible, Fathom,
|
|
85
85
|
* GA, …).
|
|
86
86
|
*
|
|
87
|
+
* **Astro**
|
|
88
|
+
* - `integrations` — Astro integrations appended after Blume's built-ins, in
|
|
89
|
+
* declaration order. Install and maintain each integration in the site.
|
|
90
|
+
*
|
|
87
91
|
* **Deployment & i18n**
|
|
88
92
|
* - `deployment` — `site` URL (needed for absolute links, sitemaps, and OG),
|
|
89
93
|
* `adapter` (`vercel`/`node`/`netlify`/`cloudflare`), `output`
|
package/src/core/data.ts
CHANGED
|
@@ -95,6 +95,7 @@ export interface BlumeDataConfig {
|
|
|
95
95
|
appleIcon: BlumeFavicon | null;
|
|
96
96
|
/** Ask AI empty-state suggestions, or `null` when Ask AI is off. */
|
|
97
97
|
ask: {
|
|
98
|
+
endpoint: string | null;
|
|
98
99
|
suggestions: NonNullable<ResolvedConfig["ai"]["ask"]>["suggestions"];
|
|
99
100
|
} | null;
|
|
100
101
|
banner: BlumeBanner | null;
|
package/src/core/graph.ts
CHANGED
|
@@ -97,6 +97,7 @@ const buildLocaleNavigation = (
|
|
|
97
97
|
path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
|
|
98
98
|
const tabs = options.navigation.tabs?.map((tab) => ({
|
|
99
99
|
...tab,
|
|
100
|
+
...(tab.href ? { href: localizePath(tab.href) } : {}),
|
|
100
101
|
items: tab.items?.map((item) => ({
|
|
101
102
|
...item,
|
|
102
103
|
path: localizePath(item.path),
|
package/src/core/navigation.ts
CHANGED
|
@@ -613,10 +613,16 @@ const resolveTabHref = (sidebar: NavNode[], path: string): string => {
|
|
|
613
613
|
return walk(sidebar) ? path : (first ?? path);
|
|
614
614
|
};
|
|
615
615
|
|
|
616
|
-
/**
|
|
616
|
+
/**
|
|
617
|
+
* Attach a resolved `href` to each tab whose section has no index page. An
|
|
618
|
+
* author-declared `href` is the tab's stated target, so it's kept as-is —
|
|
619
|
+
* resolution only fills in the tabs that didn't declare one. That's what lets a
|
|
620
|
+
* tab point at a route outside the content tree (a generated `/changelog`
|
|
621
|
+
* index, a custom `.astro` page), which resolution can't see.
|
|
622
|
+
*/
|
|
617
623
|
const withTabHrefs = (tabs: NavTab[], sidebar: NavNode[]): NavTab[] =>
|
|
618
624
|
tabs.map((tab) => {
|
|
619
|
-
const href = resolveTabHref(sidebar, tab.path);
|
|
625
|
+
const href = tab.href ?? resolveTabHref(sidebar, tab.path);
|
|
620
626
|
return href === tab.path ? tab : { ...tab, href };
|
|
621
627
|
});
|
|
622
628
|
|
|
@@ -688,6 +694,7 @@ export const buildNavigation = (
|
|
|
688
694
|
const tabs = basePath
|
|
689
695
|
? (options.tabs ?? []).map((tab) => ({
|
|
690
696
|
...tab,
|
|
697
|
+
...(tab.href ? { href: withBasePath(basePath, tab.href) } : {}),
|
|
691
698
|
items: tab.items?.map(rebasePath),
|
|
692
699
|
path: withBasePath(basePath, tab.path),
|
|
693
700
|
}))
|
package/src/core/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AstroIntegration } from "astro";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import type { ComponentMarkdown } from "../ai/component-markdown.ts";
|
|
@@ -78,6 +79,11 @@ const searchMetaSchema = z.strictObject({
|
|
|
78
79
|
tags: z.array(z.string()).optional(),
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
const aiMetaSchema = z.strictObject({
|
|
83
|
+
/** Exclude this page from llms.txt and llms-full.txt. */
|
|
84
|
+
exclude: z.boolean().default(false),
|
|
85
|
+
});
|
|
86
|
+
|
|
81
87
|
const changelogMetaSchema = z.strictObject({
|
|
82
88
|
category: z.string().optional(),
|
|
83
89
|
date: dateSchema.optional(),
|
|
@@ -105,6 +111,7 @@ const authorSchema = z.union([
|
|
|
105
111
|
|
|
106
112
|
/** Frontmatter accepted on any content page. */
|
|
107
113
|
const pageMetaBaseSchema = z.strictObject({
|
|
114
|
+
ai: aiMetaSchema.default({}),
|
|
108
115
|
/** Post author(s) for blog/changelog content; preserved, not yet rendered. */
|
|
109
116
|
authors: z.union([authorSchema, z.array(authorSchema)]).optional(),
|
|
110
117
|
changelog: changelogMetaSchema.optional(),
|
|
@@ -351,6 +358,10 @@ const contentConfigSchema = z.strictObject({
|
|
|
351
358
|
});
|
|
352
359
|
|
|
353
360
|
const navTabSchema = z.strictObject({
|
|
361
|
+
// Rejected empty rather than accepted: an empty `href` would render a link to
|
|
362
|
+
// nowhere, and it can't mean "resolve it for me" either — that's what
|
|
363
|
+
// omitting the field does.
|
|
364
|
+
href: z.string().min(1).optional(),
|
|
354
365
|
icon: iconName.optional(),
|
|
355
366
|
items: z
|
|
356
367
|
.array(
|
|
@@ -575,18 +586,44 @@ const mcpConfigSchema = z.strictObject({
|
|
|
575
586
|
route: z.string().default("/mcp").transform(normalizeRoute),
|
|
576
587
|
});
|
|
577
588
|
|
|
589
|
+
const askEndpointSchema = z
|
|
590
|
+
.string()
|
|
591
|
+
.trim()
|
|
592
|
+
.min(1)
|
|
593
|
+
.refine(
|
|
594
|
+
(value) => {
|
|
595
|
+
if (value.startsWith("/") && !value.startsWith("//")) {
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
const url = new URL(value);
|
|
600
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
message:
|
|
607
|
+
"ai.ask.endpoint must be an HTTP(S) URL or a root-relative path.",
|
|
608
|
+
}
|
|
609
|
+
);
|
|
610
|
+
|
|
578
611
|
const aiConfigSchema = z.strictObject({
|
|
579
612
|
ask: z
|
|
580
613
|
.strictObject({
|
|
581
614
|
// Name of the env var holding the provider's API key; each provider has
|
|
582
615
|
// a sensible default, so this only needs setting to override it.
|
|
583
616
|
apiKeyEnv: z.string().optional(),
|
|
584
|
-
// Base URL of the backend. Required for `openai-compatible
|
|
585
|
-
// named providers it overrides the
|
|
617
|
+
// Base URL of the backend. Required for `openai-compatible` only when no
|
|
618
|
+
// external endpoint is supplied; for named providers it overrides the preset.
|
|
586
619
|
// blume bundles Zod 3; top-level `z.url()` is undefined at runtime.
|
|
587
620
|
// oxlint-disable-next-line react-doctor/zod-v4-prefer-top-level-string-formats
|
|
588
621
|
baseUrl: z.string().url().optional(),
|
|
589
622
|
enabled: z.boolean().default(false),
|
|
623
|
+
// Optional external endpoint for projects that keep their docs static
|
|
624
|
+
// and host Ask AI in an existing backend. Absolute URLs and root-relative
|
|
625
|
+
// paths are both valid; the built-in request/stream contract is unchanged.
|
|
626
|
+
endpoint: askEndpointSchema.optional(),
|
|
590
627
|
model: z.string().default("openai/gpt-5.5"),
|
|
591
628
|
provider: z.enum(askAiProviders).default("gateway"),
|
|
592
629
|
// Empty-state prompts shown before the first question. Each renders as a
|
|
@@ -603,7 +640,10 @@ const aiConfigSchema = z.strictObject({
|
|
|
603
640
|
.superRefine((value, ctx) => {
|
|
604
641
|
// A generic OpenAI-compatible backend has no preset URL, so the user
|
|
605
642
|
// must supply one; the named providers fall back to their preset.
|
|
606
|
-
if (
|
|
643
|
+
if (
|
|
644
|
+
value.provider === "openai-compatible" &&
|
|
645
|
+
!(value.baseUrl || value.endpoint)
|
|
646
|
+
) {
|
|
607
647
|
ctx.addIssue({
|
|
608
648
|
code: z.ZodIssueCode.custom,
|
|
609
649
|
message:
|
|
@@ -1118,15 +1158,21 @@ const reactConfigSchema = z.strictObject({
|
|
|
1118
1158
|
* `http(s)` URL (OpenAPI for the Blume renderer; OpenAPI or AsyncAPI for Scalar).
|
|
1119
1159
|
*/
|
|
1120
1160
|
const openapiSourceSchema = z.strictObject({
|
|
1161
|
+
/** Include generated pages from this spec in llms.txt/llms-full.txt. */
|
|
1162
|
+
includeInLlms: z.boolean().default(true),
|
|
1163
|
+
/** Include generated pages from this spec in site search. */
|
|
1164
|
+
includeInSearch: z.boolean().default(true),
|
|
1121
1165
|
/** Nav/section label for this source. */
|
|
1122
1166
|
label: z.string().optional(),
|
|
1167
|
+
/** Emit noindex metadata and omit generated pages from the sitemap. */
|
|
1168
|
+
noindex: z.boolean().default(false),
|
|
1123
1169
|
/** Per-source route; defaults to the block's `route` (or a derived path). */
|
|
1124
1170
|
route: z.string().optional(),
|
|
1125
1171
|
/** Local path or `http(s)` URL to the spec. */
|
|
1126
1172
|
spec: z.string(),
|
|
1127
1173
|
});
|
|
1128
1174
|
|
|
1129
|
-
export type OpenApiSource = z.
|
|
1175
|
+
export type OpenApiSource = z.input<typeof openapiSourceSchema>;
|
|
1130
1176
|
|
|
1131
1177
|
/**
|
|
1132
1178
|
* Arbitrary Scalar API-reference options forwarded verbatim to the generated
|
|
@@ -1283,6 +1329,7 @@ export const blumeConfigSchema = z.strictObject({
|
|
|
1283
1329
|
frontmatter: frontmatterConfigSchema.default({}),
|
|
1284
1330
|
github: githubConfigSchema.optional(),
|
|
1285
1331
|
i18n: i18nConfigSchema.optional(),
|
|
1332
|
+
integrations: z.array(z.custom<AstroIntegration>()).default([]),
|
|
1286
1333
|
lastModified: lastModifiedConfigSchema.default(false),
|
|
1287
1334
|
logo: logoConfigSchema.optional(),
|
|
1288
1335
|
markdown: markdownConfigSchema.default({}),
|
|
@@ -7,7 +7,7 @@ import type { ResolvedConfig } from "./schema.ts";
|
|
|
7
7
|
*/
|
|
8
8
|
export const serverFeatures = (config: ResolvedConfig): string[] => {
|
|
9
9
|
const features: string[] = [];
|
|
10
|
-
if (config.ai.ask?.enabled) {
|
|
10
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
11
11
|
features.push("Ask AI");
|
|
12
12
|
}
|
|
13
13
|
// The hosted MCP server is a live JSON-RPC endpoint, so it needs a runtime.
|
package/src/core/types.ts
CHANGED
|
@@ -201,9 +201,11 @@ export interface NavTab {
|
|
|
201
201
|
*/
|
|
202
202
|
path: string;
|
|
203
203
|
/**
|
|
204
|
-
* The clickable target.
|
|
205
|
-
*
|
|
206
|
-
*
|
|
204
|
+
* The clickable target. Author-declared when the config sets it; otherwise
|
|
205
|
+
* equals `path` when the section has an index page, and resolves to the
|
|
206
|
+
* section's first page when it doesn't, so the tab doesn't link to a 404 as
|
|
207
|
+
* long as the section has a page to offer — a section with no linkable page at
|
|
208
|
+
* all keeps `path`. Absent when a resolved target matches `path`.
|
|
207
209
|
*/
|
|
208
210
|
href?: string;
|
|
209
211
|
icon?: string;
|