blume 1.7.0 → 1.7.2
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 +18 -0
- package/dist/cli/{chunk-9qs6acpw.js → chunk-12dxjqk7.js} +32 -43
- package/dist/cli/{chunk-9qs6acpw.js.map → chunk-12dxjqk7.js.map} +2 -2
- package/dist/cli/{chunk-s5dsk8bj.js → chunk-196vjxp9.js} +65 -64
- package/dist/cli/chunk-196vjxp9.js.map +13 -0
- package/dist/cli/{chunk-27gtm2ym.js → chunk-2mzebbbz.js} +1 -1
- package/dist/cli/{chunk-s5e5jt53.js → chunk-2z47ypj8.js} +1 -1
- package/dist/cli/{chunk-tnskyrej.js → chunk-30e87n55.js} +15 -24
- package/dist/cli/{chunk-tnskyrej.js.map → chunk-30e87n55.js.map} +2 -2
- package/dist/cli/{chunk-4trphnvy.js → chunk-3w7b2vcx.js} +10 -13
- package/dist/cli/{chunk-4trphnvy.js.map → chunk-3w7b2vcx.js.map} +2 -2
- package/dist/cli/{chunk-v5mm027v.js → chunk-450a7rcr.js} +8 -8
- package/dist/cli/{chunk-v5mm027v.js.map → chunk-450a7rcr.js.map} +1 -1
- package/dist/cli/{chunk-5d4q7121.js → chunk-5n7t497w.js} +142 -134
- package/dist/cli/{chunk-5d4q7121.js.map → chunk-5n7t497w.js.map} +5 -5
- package/dist/cli/{chunk-xv91q4nm.js → chunk-61j18dwk.js} +37 -9
- package/dist/cli/{chunk-xv91q4nm.js.map → chunk-61j18dwk.js.map} +3 -3
- package/dist/cli/{chunk-3r94j3tc.js → chunk-688e0dde.js} +2 -2
- package/dist/cli/{chunk-vxv4x1n8.js → chunk-88by27n5.js} +2 -2
- package/dist/cli/{chunk-cfw6x4rm.js → chunk-8cd8tj54.js} +28 -35
- package/dist/cli/{chunk-cfw6x4rm.js.map → chunk-8cd8tj54.js.map} +2 -2
- package/dist/cli/{chunk-8gnpdsn1.js → chunk-9bkjd11x.js} +2 -2
- package/dist/cli/{chunk-qq9nm3qd.js → chunk-9he6crym.js} +21 -28
- package/dist/cli/{chunk-qq9nm3qd.js.map → chunk-9he6crym.js.map} +2 -2
- package/dist/cli/{chunk-jk1zwka1.js → chunk-aztttvb3.js} +27 -33
- package/dist/cli/{chunk-jk1zwka1.js.map → chunk-aztttvb3.js.map} +2 -2
- package/dist/cli/{chunk-ckh3a410.js → chunk-cvky9gb2.js} +18 -24
- package/dist/cli/{chunk-ckh3a410.js.map → chunk-cvky9gb2.js.map} +2 -2
- package/dist/cli/{chunk-kwx90v78.js → chunk-eevwt1sc.js} +23 -32
- package/dist/cli/{chunk-kwx90v78.js.map → chunk-eevwt1sc.js.map} +2 -2
- package/dist/cli/{chunk-agy5rzxy.js → chunk-ejjx8znq.js} +126 -77
- package/dist/cli/chunk-ejjx8znq.js.map +15 -0
- package/dist/cli/{chunk-ye9zdkgv.js → chunk-exeeb35e.js} +3 -3
- package/dist/cli/{chunk-v2ymm99c.js → chunk-fmceyezb.js} +41 -50
- package/dist/cli/{chunk-v2ymm99c.js.map → chunk-fmceyezb.js.map} +2 -2
- package/dist/cli/{chunk-s102bysw.js → chunk-hdm2dkd2.js} +209 -97
- package/dist/cli/{chunk-s102bysw.js.map → chunk-hdm2dkd2.js.map} +6 -5
- package/dist/cli/{chunk-zr3ygrq3.js → chunk-hs3gbh8p.js} +10 -15
- package/dist/cli/{chunk-zr3ygrq3.js.map → chunk-hs3gbh8p.js.map} +2 -2
- package/dist/cli/{chunk-n0nyat6g.js → chunk-jbj4qhfw.js} +3 -3
- package/dist/cli/{chunk-ev67ycx0.js → chunk-jq5n4avg.js} +1 -1
- package/dist/cli/{chunk-jxkxjsc1.js → chunk-mqb2ka8m.js} +22 -30
- package/dist/cli/{chunk-jxkxjsc1.js.map → chunk-mqb2ka8m.js.map} +2 -2
- package/dist/cli/{chunk-drke6t0h.js → chunk-mt76t7dj.js} +26 -34
- package/dist/cli/{chunk-drke6t0h.js.map → chunk-mt76t7dj.js.map} +2 -2
- package/dist/cli/{chunk-jtb45atp.js → chunk-n9sra6sy.js} +13 -13
- package/dist/cli/{chunk-jtb45atp.js.map → chunk-n9sra6sy.js.map} +1 -1
- package/dist/cli/{chunk-x66c5yjn.js → chunk-ppfvdcd4.js} +2 -2
- package/dist/cli/{chunk-wd27zjcz.js → chunk-q4rae3bg.js} +1 -1
- package/dist/cli/{chunk-pxj10x8y.js → chunk-ra1v2nc2.js} +1 -1
- package/dist/cli/{chunk-y3g15rvv.js → chunk-t3tj0dgr.js} +26 -33
- package/dist/cli/{chunk-y3g15rvv.js.map → chunk-t3tj0dgr.js.map} +2 -2
- package/dist/cli/{chunk-j6pxe0dt.js → chunk-tqa1s0k8.js} +4 -4
- package/dist/cli/{chunk-cbjnx4s8.js → chunk-vh9w1sgp.js} +1 -1
- package/dist/cli/{chunk-0qhq7b8q.js → chunk-vkrsvbr5.js} +13 -17
- package/dist/cli/{chunk-0qhq7b8q.js.map → chunk-vkrsvbr5.js.map} +2 -2
- package/dist/cli/{chunk-sbdqrjbb.js → chunk-vrfp10qk.js} +1 -1
- package/dist/cli/{chunk-ynacq3ev.js → chunk-wjt80jps.js} +27 -40
- package/dist/cli/{chunk-ynacq3ev.js.map → chunk-wjt80jps.js.map} +3 -4
- package/dist/cli/{chunk-18tjv4f7.js → chunk-xhtpx3ff.js} +21 -30
- package/dist/cli/{chunk-18tjv4f7.js.map → chunk-xhtpx3ff.js.map} +2 -2
- package/dist/cli/{chunk-5hs6gb7n.js → chunk-yzhm0j9q.js} +1 -1
- package/dist/cli/index.js +397 -34
- package/dist/cli/index.js.map +12 -4
- package/dist/types/components/layout/nav-utils.d.ts +15 -2
- package/dist/types/core/config-input.d.ts +33 -0
- package/dist/types/core/schema.d.ts +29 -3
- package/docs/08-faq.mdx +21 -0
- package/docs/configuration/ask-ai.mdx +63 -0
- package/docs/content/sources.mdx +2 -2
- package/package.json +1 -1
- package/src/ai/ask.ts +31 -4
- package/src/ai/cors.ts +87 -0
- package/src/astro/generate.ts +3 -0
- package/src/astro/templates.ts +192 -83
- package/src/components/content/YouTube.astro +1 -1
- package/src/components/layout/Header.astro +1 -1
- package/src/components/layout/NavTree.astro +75 -66
- package/src/components/layout/RootLayout.astro +4 -1
- package/src/components/layout/Search.astro +11 -0
- package/src/components/layout/nav-utils.ts +47 -15
- package/src/core/adapter.ts +61 -0
- package/src/core/config-input.ts +33 -0
- package/src/core/schema.ts +61 -0
- package/src/core/sources/assets.ts +162 -26
- package/src/core/sources/notion.ts +60 -1
- package/src/registry/eject.ts +3 -0
- package/src/theme/entry.ts +9 -0
- package/dist/cli/chunk-2aj8ddew.js +0 -72
- package/dist/cli/chunk-2aj8ddew.js.map +0 -10
- package/dist/cli/chunk-4xyggvgf.js +0 -21
- package/dist/cli/chunk-4xyggvgf.js.map +0 -10
- package/dist/cli/chunk-6kzzpsx8.js +0 -26
- package/dist/cli/chunk-6kzzpsx8.js.map +0 -10
- package/dist/cli/chunk-agy5rzxy.js.map +0 -15
- package/dist/cli/chunk-bcy492zc.js +0 -16
- package/dist/cli/chunk-bcy492zc.js.map +0 -10
- package/dist/cli/chunk-btfr9yvw.js +0 -41
- package/dist/cli/chunk-btfr9yvw.js.map +0 -10
- package/dist/cli/chunk-ey89bjj1.js +0 -209
- package/dist/cli/chunk-ey89bjj1.js.map +0 -11
- package/dist/cli/chunk-s5dsk8bj.js.map +0 -13
- package/dist/cli/chunk-vt8fgygt.js +0 -23
- package/dist/cli/chunk-vt8fgygt.js.map +0 -10
- /package/dist/cli/{chunk-27gtm2ym.js.map → chunk-2mzebbbz.js.map} +0 -0
- /package/dist/cli/{chunk-s5e5jt53.js.map → chunk-2z47ypj8.js.map} +0 -0
- /package/dist/cli/{chunk-3r94j3tc.js.map → chunk-688e0dde.js.map} +0 -0
- /package/dist/cli/{chunk-vxv4x1n8.js.map → chunk-88by27n5.js.map} +0 -0
- /package/dist/cli/{chunk-8gnpdsn1.js.map → chunk-9bkjd11x.js.map} +0 -0
- /package/dist/cli/{chunk-ye9zdkgv.js.map → chunk-exeeb35e.js.map} +0 -0
- /package/dist/cli/{chunk-n0nyat6g.js.map → chunk-jbj4qhfw.js.map} +0 -0
- /package/dist/cli/{chunk-ev67ycx0.js.map → chunk-jq5n4avg.js.map} +0 -0
- /package/dist/cli/{chunk-x66c5yjn.js.map → chunk-ppfvdcd4.js.map} +0 -0
- /package/dist/cli/{chunk-wd27zjcz.js.map → chunk-q4rae3bg.js.map} +0 -0
- /package/dist/cli/{chunk-pxj10x8y.js.map → chunk-ra1v2nc2.js.map} +0 -0
- /package/dist/cli/{chunk-j6pxe0dt.js.map → chunk-tqa1s0k8.js.map} +0 -0
- /package/dist/cli/{chunk-cbjnx4s8.js.map → chunk-vh9w1sgp.js.map} +0 -0
- /package/dist/cli/{chunk-sbdqrjbb.js.map → chunk-vrfp10qk.js.map} +0 -0
- /package/dist/cli/{chunk-5hs6gb7n.js.map → chunk-yzhm0j9q.js.map} +0 -0
|
@@ -588,6 +588,8 @@ export interface AskSuggestion {
|
|
|
588
588
|
/** Backends that can route an Ask AI request. */
|
|
589
589
|
type AskProviderGateway = "gateway" | "openrouter" | "llmgateway";
|
|
590
590
|
type AskProvider = AskProviderGateway | "inkeep" | "openai-compatible";
|
|
591
|
+
/** How much the model reasons before answering (`ai.ask.reasoning`). */
|
|
592
|
+
type AskReasoning = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
591
593
|
/** How much retrieved documentation each Ask AI question carries. */
|
|
592
594
|
export interface AskRetrievalConfig {
|
|
593
595
|
/**
|
|
@@ -620,6 +622,18 @@ export interface AskConfig {
|
|
|
620
622
|
* overrides the built-in preset.
|
|
621
623
|
*/
|
|
622
624
|
baseUrl?: string;
|
|
625
|
+
/**
|
|
626
|
+
* Origins allowed to call the generated endpoint from another site — a
|
|
627
|
+
* marketing page that embeds an ask box, for example — or `"*"` to allow
|
|
628
|
+
* every origin. The route answers preflight requests and names a listed
|
|
629
|
+
* origin on every response, errors included; every other origin stays
|
|
630
|
+
* subject to the browser's same-origin rule. Callers must send the body as
|
|
631
|
+
* JSON with a `content-type: application/json` header, or Astro's cross-site
|
|
632
|
+
* request check rejects the `POST` before the route runs. Only the generated
|
|
633
|
+
* route reads this; an external `endpoint` handles its own CORS and can't be
|
|
634
|
+
* combined with it.
|
|
635
|
+
*/
|
|
636
|
+
cors?: string[];
|
|
623
637
|
/** Turn Ask AI on. Defaults to `false`. */
|
|
624
638
|
enabled?: boolean;
|
|
625
639
|
/**
|
|
@@ -628,6 +642,13 @@ export interface AskConfig {
|
|
|
628
642
|
* limiting, and streaming. Accepts an absolute URL or root-relative path.
|
|
629
643
|
*/
|
|
630
644
|
endpoint?: string;
|
|
645
|
+
/**
|
|
646
|
+
* Static request headers sent to the provider on every call — a
|
|
647
|
+
* caller-identifying header for a shared backend, for example. Values are
|
|
648
|
+
* written into the generated route as literals, so keep secrets in
|
|
649
|
+
* `apiKeyEnv` rather than here.
|
|
650
|
+
*/
|
|
651
|
+
headers?: Record<string, string>;
|
|
631
652
|
/**
|
|
632
653
|
* Extra system-prompt text appended to the built-in instructions — use it
|
|
633
654
|
* for identity, language, or tone. The built-in grounding behavior (answer
|
|
@@ -638,6 +659,18 @@ export interface AskConfig {
|
|
|
638
659
|
model?: string;
|
|
639
660
|
/** Which backend routes the request. Defaults to `gateway`. */
|
|
640
661
|
provider?: AskProvider;
|
|
662
|
+
/**
|
|
663
|
+
* How much the model reasons before answering, from `"none"` to `"xhigh"`.
|
|
664
|
+
* Sent as the backend's own reasoning-effort control: the AI SDK's
|
|
665
|
+
* `reasoning` option on the gateway, `reasoning.effort` on OpenRouter, and
|
|
666
|
+
* `reasoning_effort` on OpenAI-compatible endpoints. The model has to
|
|
667
|
+
* support the level — OpenAI rejects one a model doesn't offer — and the
|
|
668
|
+
* endpoint has to accept the parameter; Inkeep has no reasoning control,
|
|
669
|
+
* so the field is rejected there. Omitted keeps the model's default.
|
|
670
|
+
* `"none"` is the fastest and cheapest for grounded docs Q&A, where the
|
|
671
|
+
* retrieved excerpts carry the answer.
|
|
672
|
+
*/
|
|
673
|
+
reasoning?: AskReasoning;
|
|
641
674
|
/**
|
|
642
675
|
* How much documentation each question carries into the model's prompt.
|
|
643
676
|
* Lower values cut time-to-first-token — which dominates on a self-hosted
|
|
@@ -327,6 +327,11 @@ export type SidebarItemConfig = string | {
|
|
|
327
327
|
root?: string;
|
|
328
328
|
};
|
|
329
329
|
export declare const searchProviders: readonly ["orama", "pagefind", "flexsearch", "algolia", "orama-cloud", "typesense", "mixedbread", "none"];
|
|
330
|
+
/**
|
|
331
|
+
* The `ai.ask.reasoning` levels: the AI SDK's top-level `reasoning` values
|
|
332
|
+
* minus `provider-default`, which is what omitting the field means.
|
|
333
|
+
*/
|
|
334
|
+
export declare const askReasoningLevels: readonly ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
330
335
|
/** Ask AI backends. `gateway` (default) routes through the Vercel AI Gateway. */
|
|
331
336
|
export declare const askAiProviders: readonly ["gateway", "openrouter", "llmgateway", "inkeep", "openai-compatible"];
|
|
332
337
|
declare const aiConfigSchema: z.ZodObject<{
|
|
@@ -334,8 +339,10 @@ declare const aiConfigSchema: z.ZodObject<{
|
|
|
334
339
|
ask: z.ZodOptional<z.ZodObject<{
|
|
335
340
|
apiKeyEnv: z.ZodOptional<z.ZodString>;
|
|
336
341
|
baseUrl: z.ZodOptional<z.ZodURL>;
|
|
342
|
+
cors: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodPipe<z.ZodURL, z.ZodTransform<string, string>>]>>>;
|
|
337
343
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
338
344
|
endpoint: z.ZodOptional<z.ZodString>;
|
|
345
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
339
346
|
instructions: z.ZodOptional<z.ZodString>;
|
|
340
347
|
model: z.ZodDefault<z.ZodString>;
|
|
341
348
|
provider: z.ZodDefault<z.ZodEnum<{
|
|
@@ -345,6 +352,14 @@ declare const aiConfigSchema: z.ZodObject<{
|
|
|
345
352
|
inkeep: "inkeep";
|
|
346
353
|
"openai-compatible": "openai-compatible";
|
|
347
354
|
}>>;
|
|
355
|
+
reasoning: z.ZodOptional<z.ZodEnum<{
|
|
356
|
+
none: "none";
|
|
357
|
+
minimal: "minimal";
|
|
358
|
+
low: "low";
|
|
359
|
+
medium: "medium";
|
|
360
|
+
high: "high";
|
|
361
|
+
xhigh: "xhigh";
|
|
362
|
+
}>>;
|
|
348
363
|
retrieval: z.ZodOptional<z.ZodObject<{
|
|
349
364
|
contextBudget: z.ZodOptional<z.ZodNumber>;
|
|
350
365
|
excerptChars: z.ZodOptional<z.ZodNumber>;
|
|
@@ -390,6 +405,7 @@ declare const aiConfigSchema: z.ZodObject<{
|
|
|
390
405
|
webmcp: z.ZodDefault<z.ZodBoolean>;
|
|
391
406
|
}, z.core.$strict>;
|
|
392
407
|
export type AskAiProvider = (typeof askAiProviders)[number];
|
|
408
|
+
export type AskReasoning = (typeof askReasoningLevels)[number];
|
|
393
409
|
export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
|
|
394
410
|
export { openInChatProviders } from "./open-in-chat.ts";
|
|
395
411
|
export type { OpenInChatProvider } from "./open-in-chat.ts";
|
|
@@ -504,9 +520,9 @@ declare const contentSignalsSchema: z.ZodPipe<z.ZodUnion<readonly [z.ZodBoolean,
|
|
|
504
520
|
declare const dateFormatConfigSchema: z.ZodObject<{
|
|
505
521
|
calendar: z.ZodOptional<z.ZodString>;
|
|
506
522
|
dateStyle: z.ZodOptional<z.ZodEnum<{
|
|
523
|
+
medium: "medium";
|
|
507
524
|
full: "full";
|
|
508
525
|
long: "long";
|
|
509
|
-
medium: "medium";
|
|
510
526
|
short: "short";
|
|
511
527
|
}>>;
|
|
512
528
|
day: z.ZodOptional<z.ZodEnum<{
|
|
@@ -575,8 +591,10 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
575
591
|
ask: z.ZodOptional<z.ZodObject<{
|
|
576
592
|
apiKeyEnv: z.ZodOptional<z.ZodString>;
|
|
577
593
|
baseUrl: z.ZodOptional<z.ZodURL>;
|
|
594
|
+
cors: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodPipe<z.ZodURL, z.ZodTransform<string, string>>]>>>;
|
|
578
595
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
579
596
|
endpoint: z.ZodOptional<z.ZodString>;
|
|
597
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
580
598
|
instructions: z.ZodOptional<z.ZodString>;
|
|
581
599
|
model: z.ZodDefault<z.ZodString>;
|
|
582
600
|
provider: z.ZodDefault<z.ZodEnum<{
|
|
@@ -586,6 +604,14 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
586
604
|
inkeep: "inkeep";
|
|
587
605
|
"openai-compatible": "openai-compatible";
|
|
588
606
|
}>>;
|
|
607
|
+
reasoning: z.ZodOptional<z.ZodEnum<{
|
|
608
|
+
none: "none";
|
|
609
|
+
minimal: "minimal";
|
|
610
|
+
low: "low";
|
|
611
|
+
medium: "medium";
|
|
612
|
+
high: "high";
|
|
613
|
+
xhigh: "xhigh";
|
|
614
|
+
}>>;
|
|
589
615
|
retrieval: z.ZodOptional<z.ZodObject<{
|
|
590
616
|
contextBudget: z.ZodOptional<z.ZodNumber>;
|
|
591
617
|
excerptChars: z.ZodOptional<z.ZodNumber>;
|
|
@@ -771,9 +797,9 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
771
797
|
dateFormat: z.ZodDefault<z.ZodObject<{
|
|
772
798
|
calendar: z.ZodOptional<z.ZodString>;
|
|
773
799
|
dateStyle: z.ZodOptional<z.ZodEnum<{
|
|
800
|
+
medium: "medium";
|
|
774
801
|
full: "full";
|
|
775
802
|
long: "long";
|
|
776
|
-
medium: "medium";
|
|
777
803
|
short: "short";
|
|
778
804
|
}>>;
|
|
779
805
|
day: z.ZodOptional<z.ZodEnum<{
|
|
@@ -1059,6 +1085,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1059
1085
|
label: z.ZodString;
|
|
1060
1086
|
}, z.core.$strict>>>;
|
|
1061
1087
|
provider: z.ZodDefault<z.ZodEnum<{
|
|
1088
|
+
none: "none";
|
|
1062
1089
|
algolia: "algolia";
|
|
1063
1090
|
mixedbread: "mixedbread";
|
|
1064
1091
|
orama: "orama";
|
|
@@ -1066,7 +1093,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1066
1093
|
flexsearch: "flexsearch";
|
|
1067
1094
|
"orama-cloud": "orama-cloud";
|
|
1068
1095
|
typesense: "typesense";
|
|
1069
|
-
none: "none";
|
|
1070
1096
|
}>>;
|
|
1071
1097
|
typesense: z.ZodOptional<z.ZodObject<{
|
|
1072
1098
|
collection: z.ZodString;
|
package/docs/08-faq.mdx
CHANGED
|
@@ -160,3 +160,24 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
|
|
|
160
160
|
:::warning[Version-pinned]
|
|
161
161
|
The patch targets a specific oxfmt build — its diff references a file whose name is hashed per release (`dist/markdown-*.js`). When you bump oxfmt, regenerate the patch (e.g. `bun patch oxfmt`) or check whether the upstream fix has landed and the patch is no longer needed.
|
|
162
162
|
:::
|
|
163
|
+
|
|
164
|
+
## Why does Knip report my `blume.config.ts` dependencies as unused?
|
|
165
|
+
|
|
166
|
+
[Knip](https://knip.dev) only follows imports from files it knows are entry points, and it learns those from its built-in plugins. There is no Blume plugin yet, and Knip's Astro plugin doesn't switch on either: it looks for `astro` in your own `package.json`, but a Blume project depends on `blume`, and the generated `.blume/` Astro project is gitignored, so Knip never sees it. Nothing references `blume.config.ts`, so any package it imports gets reported as unused.
|
|
167
|
+
|
|
168
|
+
Register the files Blume loads from your project root as entries. In `knip.json`:
|
|
169
|
+
|
|
170
|
+
```json knip.json
|
|
171
|
+
{
|
|
172
|
+
"entry": [
|
|
173
|
+
"blume.config.{ts,mjs,js}",
|
|
174
|
+
"components.{ts,tsx}",
|
|
175
|
+
"islands/**/*.{ts,tsx}",
|
|
176
|
+
"pages/**/*"
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
In a monorepo, put the same `entry` list under the docs workspace in `workspaces` instead. Drop any line for a convention you don't use — `components.ts` for [component overrides](/docs/configuration/customization#component-overrides), `islands/` for [interactive islands](/docs/configuration/customization#interactive-islands), and `pages/` for [custom pages](/docs/configuration/customization#custom-pages) (adjust the last one if you've changed `content.pages`).
|
|
182
|
+
|
|
183
|
+
Knip can only follow real imports. A package that's only named inside a string — say, an [Astro integration](/docs/configuration/customization#astro-integrations) that calls `injectScript("page", "import('some-package')")` — still needs an `ignoreDependencies` entry.
|
|
@@ -109,6 +109,37 @@ Blume sends the same `POST` body as its built-in route:
|
|
|
109
109
|
|
|
110
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
111
|
|
|
112
|
+
## Cross-origin callers
|
|
113
|
+
|
|
114
|
+
The generated endpoint answers the in-page assistant on its own origin. To call it from another site as well — a marketing page with an ask box, say — list that site's origin in `cors`:
|
|
115
|
+
|
|
116
|
+
```ts blume.config.ts lineNumbers
|
|
117
|
+
ai: {
|
|
118
|
+
ask: {
|
|
119
|
+
enabled: true,
|
|
120
|
+
cors: ["https://www.example.com"],
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The route then answers the browser's `OPTIONS` preflight and names a listed origin on every response — the streamed answer and the error statuses alike, so the caller can tell a rejected body from a provider failure. Origins that aren't listed get no header and stay subject to the browser's same-origin rule. Each entry is reduced to its origin, so `https://www.example.com/docs/` and `https://www.example.com` mean the same thing. To let any page call the route, list `"*"` instead of origins.
|
|
126
|
+
|
|
127
|
+
The caller sends the same `POST` body the [external endpoint](#external-endpoint) contract describes and reads back the same text stream. Send it as JSON with a `content-type: application/json` header:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const response = await fetch("https://docs.example.com/api/ask", {
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
messages: [{ role: "user", content: "How do I deploy?" }],
|
|
133
|
+
}),
|
|
134
|
+
headers: { "content-type": "application/json" },
|
|
135
|
+
method: "POST",
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The content type matters: Astro's cross-site request check rejects a cross-origin `POST` that has no content type, or a form-like one such as `text/plain`, with a 403 before the route runs, and that response carries no CORS headers, so the browser reports it as a network error rather than a status. The preflight allows whatever request headers the caller asks for, so a fetch wrapper that adds its own headers needs no extra configuration.
|
|
140
|
+
|
|
141
|
+
`cors` only affects the generated route; with an external `endpoint`, CORS is that backend's job, and setting both is a config error. The endpoint stays unauthenticated either way, so the [rate limiting](#rate-limiting) advice applies to cross-origin traffic too.
|
|
142
|
+
|
|
112
143
|
## Server output required
|
|
113
144
|
|
|
114
145
|
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:
|
|
@@ -166,12 +197,44 @@ ai: {
|
|
|
166
197
|
|
|
167
198
|
Set `apiKeyEnv` (and, for the named providers, `baseUrl`) on any backend to point at a different env var or proxy.
|
|
168
199
|
|
|
200
|
+
To send static request headers with every call — a caller-identifying header for a shared backend, say, so its own observability or rate limiting can tell your docs apart from other traffic — set `headers`. It works on every backend, including the gateway:
|
|
201
|
+
|
|
202
|
+
```ts blume.config.ts lineNumbers
|
|
203
|
+
ai: {
|
|
204
|
+
ask: {
|
|
205
|
+
enabled: true,
|
|
206
|
+
provider: "openai-compatible",
|
|
207
|
+
baseUrl: "https://llm.internal.example.com/v1",
|
|
208
|
+
apiKeyEnv: "INTERNAL_LLM_API_KEY",
|
|
209
|
+
headers: { "X-Caller-Id": "docs" },
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The values are written into the generated route as-is, so keep secrets in `apiKeyEnv` rather than in `headers`. The API key's `Authorization` header is applied first, so a custom header can't displace it.
|
|
215
|
+
|
|
169
216
|
:::note
|
|
170
217
|
**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
218
|
:::
|
|
172
219
|
|
|
173
220
|
Keys are read through Astro's [`getSecret()`](https://docs.astro.build/en/guides/environment-variables/#retrieving-secrets-programmatically), so each adapter supplies them its own way: environment variables on Node, Vercel, and Netlify, and the Worker's [bindings](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets) on Cloudflare. Enabling Ask AI also turns on React for the in-page island — see [Customization](/docs/configuration/customization#interactive-islands).
|
|
174
221
|
|
|
222
|
+
## Reasoning
|
|
223
|
+
|
|
224
|
+
Reasoning models think before they answer, and how much they do so by default varies by model. For grounded docs Q&A the retrieved excerpts carry the answer, so most of that thinking is latency the reader waits through. `reasoning` sets how much the model reasons: `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, or `"xhigh"`:
|
|
225
|
+
|
|
226
|
+
```ts blume.config.ts lineNumbers
|
|
227
|
+
ai: {
|
|
228
|
+
ask: {
|
|
229
|
+
enabled: true,
|
|
230
|
+
model: "openai/gpt-5.5",
|
|
231
|
+
reasoning: "none",
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The value is sent as the backend's own reasoning-effort control. Through the gateway it travels as the [AI SDK's `reasoning` option](https://ai-sdk.dev/docs/ai-sdk-core/reasoning), which the gateway maps to the model's setting — OpenAI's `reasoning_effort`, for example. On OpenRouter it is sent as `reasoning.effort`, and on LLMGateway or a custom `openai-compatible` endpoint as `reasoning_effort` in the request, so the endpoint has to accept that parameter. The model has to support the level you pick: OpenAI rejects a level a model doesn't offer (`"none"` and `"xhigh"` exist only on some), so check the model's documentation before setting one. Inkeep runs its own QA pipeline and has no reasoning control, so setting `reasoning` with that backend is a config error. Leave it unset to keep the model's default. Like [retrieval size](#retrieval-size), it trades thoroughness for time-to-first-token, and answers stay grounded either way.
|
|
237
|
+
|
|
175
238
|
## Rate limiting
|
|
176
239
|
|
|
177
240
|
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.
|
package/docs/content/sources.mdx
CHANGED
|
@@ -163,7 +163,7 @@ A read token for a private dataset comes from the `SANITY_TOKEN` environment var
|
|
|
163
163
|
|
|
164
164
|
## Notion
|
|
165
165
|
|
|
166
|
-
The built-in `notion` source turns a Notion database into a collection: each row becomes a page, its properties become frontmatter, and its block tree becomes MDX. Callouts, toggles, columns, and code blocks map to the matching Blume components. `@notionhq/client` (v5 or later) is an optional peer dependency; Blume reads the database through its first data source.
|
|
166
|
+
The built-in `notion` source turns a Notion database into a collection: each row becomes a page, its properties become frontmatter, and its block tree becomes MDX. Callouts, toggles, columns, and code blocks map to the matching Blume components. Video blocks become a `<YouTube>` embed when they hold a YouTube link and a `<video>` player otherwise, with the block's caption as a `<Frame>` caption either way; a link to a video page rather than a media file (a Vimeo or Loom URL, say) is reported as a warning instead of embedded. `@notionhq/client` (v5 or later) is an optional peer dependency; Blume reads the database through its first data source.
|
|
167
167
|
|
|
168
168
|
```ts blume.config.ts
|
|
169
169
|
import { defineConfig } from "blume";
|
|
@@ -185,7 +185,7 @@ export default defineConfig({
|
|
|
185
185
|
});
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
The integration token comes from the `NOTION_TOKEN` environment variable (share the database with your integration). By default every page is imported; set `publishedValue` to make the `Status` property a publish gate — any other value then maps to `draft: true`, which production builds drop. **Notion image URLs are signed and expire**, so the adapter downloads them at build time into the site's assets and rewrites the references — a CMS
|
|
188
|
+
The integration token comes from the `NOTION_TOKEN` environment variable (share the database with your integration). By default every page is imported; set `publishedValue` to make the `Status` property a publish gate — any other value then maps to `draft: true`, which production builds drop. **Notion image and video URLs are signed and expire**, so the adapter downloads them at build time into the site's assets and rewrites the references — a CMS asset never rots a static build. API calls are paced through a small request pool (3 at a time, matching Notion's per-integration rate limit) so databases with hundreds of pages import without tripping `429` responses; set `concurrency` on the source to tune it.
|
|
189
189
|
|
|
190
190
|
## Preview and sync
|
|
191
191
|
|
package/package.json
CHANGED
package/src/ai/ask.ts
CHANGED
|
@@ -7,16 +7,31 @@ import type { AskAiConfig } from "../core/schema.ts";
|
|
|
7
7
|
* AI SDK's OpenAI-compatible provider.
|
|
8
8
|
*/
|
|
9
9
|
export type AskBackend =
|
|
10
|
-
| { kind: "gateway"; model: string }
|
|
11
|
-
| {
|
|
10
|
+
| { headers?: AskHeaders; kind: "gateway"; model: string }
|
|
11
|
+
| {
|
|
12
|
+
apiKeyEnv: string;
|
|
13
|
+
headers?: AskHeaders;
|
|
14
|
+
kind: "openrouter";
|
|
15
|
+
model: string;
|
|
16
|
+
}
|
|
12
17
|
| {
|
|
13
18
|
apiKeyEnv: string;
|
|
14
19
|
baseUrl: string;
|
|
20
|
+
headers?: AskHeaders;
|
|
15
21
|
kind: "openai-compatible";
|
|
16
22
|
model: string;
|
|
17
23
|
name: string;
|
|
18
24
|
};
|
|
19
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Static request headers (`ai.ask.headers`) every backend forwards to its
|
|
28
|
+
* provider factory. Every provider Blume generates against accepts the same
|
|
29
|
+
* `headers` option, so the map travels unchanged; the OpenAI-compatible
|
|
30
|
+
* provider applies them after the `Authorization` header it derives from the
|
|
31
|
+
* API key, so a custom header can't displace auth.
|
|
32
|
+
*/
|
|
33
|
+
export type AskHeaders = Record<string, string>;
|
|
34
|
+
|
|
20
35
|
interface AskPreset {
|
|
21
36
|
apiKeyEnv: string;
|
|
22
37
|
baseUrl?: string;
|
|
@@ -69,17 +84,28 @@ const ASK_PRESETS: AskPresetRegistry = {
|
|
|
69
84
|
|
|
70
85
|
const DEFAULT_MODEL = "openai/gpt-5.5";
|
|
71
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The `headers` field every backend variant shares. An empty map is dropped so
|
|
89
|
+
* the generated route only carries a `headers` option when there is something
|
|
90
|
+
* to send.
|
|
91
|
+
*/
|
|
92
|
+
const askHeadersField = (ask?: AskAiConfig): { headers?: AskHeaders } =>
|
|
93
|
+
ask?.headers && Object.keys(ask.headers).length > 0
|
|
94
|
+
? { headers: ask.headers }
|
|
95
|
+
: {};
|
|
96
|
+
|
|
72
97
|
/** Resolve the `ai.ask` config into the backend the endpoint is built against. */
|
|
73
98
|
export const resolveAskBackend = (ask?: AskAiConfig): AskBackend => {
|
|
74
99
|
const provider = ask?.provider ?? "gateway";
|
|
75
100
|
const model = ask?.model ?? DEFAULT_MODEL;
|
|
101
|
+
const headers = askHeadersField(ask);
|
|
76
102
|
if (provider === "gateway") {
|
|
77
|
-
return { kind: "gateway", model };
|
|
103
|
+
return { ...headers, kind: "gateway", model };
|
|
78
104
|
}
|
|
79
105
|
const preset = ASK_PRESETS[provider];
|
|
80
106
|
const apiKeyEnv = ask?.apiKeyEnv ?? preset?.apiKeyEnv ?? "API_KEY";
|
|
81
107
|
if (provider === "openrouter") {
|
|
82
|
-
return { apiKeyEnv, kind: "openrouter", model };
|
|
108
|
+
return { apiKeyEnv, ...headers, kind: "openrouter", model };
|
|
83
109
|
}
|
|
84
110
|
// `llmgateway`, `inkeep`, and the generic `openai-compatible` provider all
|
|
85
111
|
// stream through the AI SDK's OpenAI-compatible provider. The schema requires
|
|
@@ -87,6 +113,7 @@ export const resolveAskBackend = (ask?: AskAiConfig): AskBackend => {
|
|
|
87
113
|
return {
|
|
88
114
|
apiKeyEnv,
|
|
89
115
|
baseUrl: ask?.baseUrl ?? preset?.baseUrl ?? "",
|
|
116
|
+
...headers,
|
|
90
117
|
kind: OPENAI_COMPATIBLE,
|
|
91
118
|
model,
|
|
92
119
|
name: preset?.name ?? OPENAI_COMPATIBLE,
|
package/src/ai/cors.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS for the generated Ask AI route (`ai.ask.cors`).
|
|
3
|
+
*
|
|
4
|
+
* A browser only lets a page on another origin read a response that names
|
|
5
|
+
* that origin, and a JSON `POST` preflights first. `preflightResponse` answers
|
|
6
|
+
* the `OPTIONS`; `withCors` wraps the `POST` handler so every response it
|
|
7
|
+
* returns — the stream, a 400, a 500 — carries the headers. Wrapping once,
|
|
8
|
+
* rather than stamping each `return`, keeps the next return site added to the
|
|
9
|
+
* handler from shipping an opaque failure for that one status.
|
|
10
|
+
*
|
|
11
|
+
* `allowed` is the `ai.ask.cors` list: origins already reduced to their
|
|
12
|
+
* `scheme://host[:port]` form by the config schema, or the single entry `"*"`
|
|
13
|
+
* to admit every origin.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The `ai.ask.cors` entry that admits every origin. */
|
|
17
|
+
export const ANY_ORIGIN = "*";
|
|
18
|
+
|
|
19
|
+
/** The headers a response carries for a cross-origin caller. */
|
|
20
|
+
export interface CorsHeaders {
|
|
21
|
+
"access-control-allow-origin"?: string;
|
|
22
|
+
vary?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The response headers that name the caller's origin when `allowed` lists it. */
|
|
26
|
+
export const corsHeaders = (
|
|
27
|
+
request: Request,
|
|
28
|
+
allowed: readonly string[]
|
|
29
|
+
): CorsHeaders => {
|
|
30
|
+
if (allowed.includes(ANY_ORIGIN)) {
|
|
31
|
+
// A wildcard answer is the same for every caller, so nothing to vary on.
|
|
32
|
+
return { "access-control-allow-origin": ANY_ORIGIN };
|
|
33
|
+
}
|
|
34
|
+
// `Vary` rides on both branches: the answer depends on `Origin` whether or
|
|
35
|
+
// not it was listed, so a shared cache never hands one origin's response
|
|
36
|
+
// (or the header-less one) to another.
|
|
37
|
+
const origin = request.headers.get("origin");
|
|
38
|
+
return origin && allowed.includes(origin)
|
|
39
|
+
? { "access-control-allow-origin": origin, vary: "origin" }
|
|
40
|
+
: { vary: "origin" };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Answer the browser's `OPTIONS` preflight for the route. */
|
|
44
|
+
export const preflightResponse = (
|
|
45
|
+
request: Request,
|
|
46
|
+
allowed: readonly string[]
|
|
47
|
+
): Response =>
|
|
48
|
+
new Response(null, {
|
|
49
|
+
headers: {
|
|
50
|
+
...corsHeaders(request, allowed),
|
|
51
|
+
// Reflect whatever the caller's fetch wrapper asks to send, falling back
|
|
52
|
+
// to the JSON POST's own `content-type`; a listed origin shouldn't need
|
|
53
|
+
// an eject to add a header of its own.
|
|
54
|
+
"access-control-allow-headers":
|
|
55
|
+
request.headers.get("access-control-request-headers") ?? "content-type",
|
|
56
|
+
"access-control-allow-methods": "POST",
|
|
57
|
+
"access-control-max-age": "86400",
|
|
58
|
+
},
|
|
59
|
+
status: 204,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** The slice of Astro's `APIContext` the wrapped handler reads. */
|
|
63
|
+
interface RequestContext {
|
|
64
|
+
request: Request;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Stamp the CORS headers on every response `handler` returns. */
|
|
68
|
+
export const withCors =
|
|
69
|
+
(
|
|
70
|
+
allowed: readonly string[],
|
|
71
|
+
handler: (context: RequestContext) => Promise<Response> | Response
|
|
72
|
+
): ((context: RequestContext) => Promise<Response>) =>
|
|
73
|
+
async (context) => {
|
|
74
|
+
const response = await handler(context);
|
|
75
|
+
for (const [key, value] of Object.entries(
|
|
76
|
+
corsHeaders(context.request, allowed)
|
|
77
|
+
)) {
|
|
78
|
+
// `Vary` accumulates (the handler may already vary on something), the
|
|
79
|
+
// rest replace.
|
|
80
|
+
if (key === "vary") {
|
|
81
|
+
response.headers.append(key, value);
|
|
82
|
+
} else {
|
|
83
|
+
response.headers.set(key, value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return response;
|
|
87
|
+
};
|
package/src/astro/generate.ts
CHANGED
|
@@ -1826,7 +1826,9 @@ const writeAskFiles = async (
|
|
|
1826
1826
|
await write(
|
|
1827
1827
|
join(srcDir, "pages", "api", "ask.ts"),
|
|
1828
1828
|
askEndpointTemplate(resolveAskBackend(ask), grounded, {
|
|
1829
|
+
cors: ask.cors,
|
|
1829
1830
|
instructions: ask.instructions,
|
|
1831
|
+
reasoning: ask.reasoning,
|
|
1830
1832
|
retrieval: ask.retrieval,
|
|
1831
1833
|
})
|
|
1832
1834
|
);
|
|
@@ -2339,6 +2341,7 @@ export const generateRuntime = async (
|
|
|
2339
2341
|
changelogIndexTemplate({
|
|
2340
2342
|
exportEpub,
|
|
2341
2343
|
exportPdf,
|
|
2344
|
+
mathEnabled: usesMath,
|
|
2342
2345
|
needsReact,
|
|
2343
2346
|
staged: hasStaged,
|
|
2344
2347
|
})
|