blume 2.2.0 → 2.2.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 +15 -0
- package/dist/cli/{chunk-xgq4eytj.js → chunk-080mfch6.js} +15 -15
- package/dist/cli/{chunk-br0n8n3e.js → chunk-1ch3ct3r.js} +14 -14
- package/dist/cli/{chunk-49r370mb.js → chunk-2ssh60qh.js} +6 -6
- package/dist/cli/{chunk-1cm0h819.js → chunk-34wap3ed.js} +3 -3
- package/dist/cli/{chunk-fqy4n3q2.js → chunk-40vk8kgf.js} +7 -7
- package/dist/cli/{chunk-96snx8yy.js → chunk-487b95qy.js} +3 -3
- package/dist/cli/{chunk-kkrteb15.js → chunk-4zxh25hc.js} +2 -2
- package/dist/cli/{chunk-cn3dy3hz.js → chunk-69rjf786.js} +5 -5
- package/dist/cli/{chunk-dmx6cezx.js → chunk-9h8mh2tk.js} +3 -3
- package/dist/cli/{chunk-dmx6cezx.js.map → chunk-9h8mh2tk.js.map} +2 -2
- package/dist/cli/{chunk-t5xx74rd.js → chunk-9jqmq5hv.js} +6 -6
- package/dist/cli/{chunk-8b489v1b.js → chunk-aafhwx74.js} +1 -1
- package/dist/cli/{chunk-2nr29y95.js → chunk-b680sv30.js} +3 -3
- package/dist/cli/{chunk-rkqae7kh.js → chunk-beb0z983.js} +16 -16
- package/dist/cli/{chunk-th0pgaj5.js → chunk-c5bjxq8v.js} +2 -2
- package/dist/cli/{chunk-wehv26ry.js → chunk-e1n41w9m.js} +3 -3
- package/dist/cli/{chunk-frk2wdq1.js → chunk-etckvbrh.js} +14 -14
- package/dist/cli/{chunk-ahc5gc9h.js → chunk-f260jdrn.js} +5 -5
- package/dist/cli/{chunk-05f21r8h.js → chunk-fnxgtjeq.js} +1 -1
- package/dist/cli/{chunk-4rftmttv.js → chunk-gwy55cx3.js} +2 -2
- package/dist/cli/{chunk-4rftmttv.js.map → chunk-gwy55cx3.js.map} +2 -2
- package/dist/cli/{chunk-vnbvhgmd.js → chunk-gxhcf2hs.js} +2 -2
- package/dist/cli/{chunk-bjk964fp.js → chunk-h0qwdbem.js} +2 -2
- package/dist/cli/{chunk-1rkxzg3n.js → chunk-hktq4j5r.js} +15 -12
- package/dist/cli/{chunk-1rkxzg3n.js.map → chunk-hktq4j5r.js.map} +3 -3
- package/dist/cli/{chunk-enk76qd5.js → chunk-hn3g2b89.js} +9 -9
- package/dist/cli/{chunk-w6e455pz.js → chunk-j4nfgn2j.js} +1 -1
- package/dist/cli/{chunk-as09pafp.js → chunk-k45sq16q.js} +4 -4
- package/dist/cli/{chunk-as09pafp.js.map → chunk-k45sq16q.js.map} +3 -3
- package/dist/cli/{chunk-p8zg3a2h.js → chunk-kmswjxj9.js} +3 -3
- package/dist/cli/{chunk-xt0qvz1t.js → chunk-nqna8dzp.js} +17 -17
- package/dist/cli/{chunk-dyk3r7jw.js → chunk-p834rkcn.js} +5 -5
- package/dist/cli/{chunk-dpcahcrb.js → chunk-ppa6mg4d.js} +6 -6
- package/dist/cli/{chunk-79bzzkx4.js → chunk-q1wwrxzw.js} +2 -2
- package/dist/cli/{chunk-m9bp2hc1.js → chunk-qcyxkj44.js} +1 -1
- package/dist/cli/{chunk-dm4mg9bj.js → chunk-r5nm9w0c.js} +740 -685
- package/dist/cli/{chunk-dm4mg9bj.js.map → chunk-r5nm9w0c.js.map} +8 -6
- package/dist/cli/{chunk-a3ydrqny.js → chunk-rtvgmg2j.js} +2 -2
- package/dist/cli/{chunk-c69tw29j.js → chunk-t2h8pwy1.js} +4 -4
- package/dist/cli/{chunk-vzy9rh6y.js → chunk-x60e15st.js} +17 -17
- package/dist/cli/{chunk-mrvh4c88.js → chunk-x7bh6d9j.js} +4 -4
- package/dist/cli/{chunk-7tekknev.js → chunk-ydqzkhjn.js} +6 -6
- package/dist/cli/{chunk-gprwej7h.js → chunk-yg8ckc3g.js} +2 -2
- package/dist/cli/{chunk-8f8xw4x2.js → chunk-zxd5jrgx.js} +14 -4
- package/dist/cli/{chunk-8f8xw4x2.js.map → chunk-zxd5jrgx.js.map} +3 -3
- package/dist/cli/index.js +18 -18
- package/dist/types/core/config-input.d.ts +37 -5
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/mcp-clients.d.ts +47 -0
- package/dist/types/core/nav-diagnostics.d.ts +7 -0
- package/dist/types/core/schema.d.ts +55 -4
- package/dist/types/ratelimit/index.d.ts +2 -0
- package/dist/types/ratelimit/memory.d.ts +1 -1
- package/dist/types/ratelimit/schema.d.ts +40 -0
- package/dist/types/ratelimit/unkey.d.ts +48 -0
- package/docs/02-deployment.mdx +1 -0
- package/docs/configuration/index.mdx +5 -4
- package/docs/configuration/rate-limiting.mdx +20 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/discoverability/mcp.mdx +40 -8
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +2 -2
- package/skills/blume-migrate/references/gitbook.md +2 -2
- package/skills/blume-migrate/references/github-wiki.md +2 -2
- package/skills/blume-migrate/references/jekyll.md +2 -2
- package/skills/blume-migrate/references/mkdocs.md +2 -2
- package/skills/blume-migrate/references/nextra.md +3 -3
- package/skills/blume-migrate/references/readme.md +1 -1
- package/skills/blume-migrate/references/redocly.md +2 -2
- package/skills/blume-migrate/references/vitepress.md +2 -2
- package/skills/blume-migrate/references/vuepress.md +1 -1
- package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +1 -1
- package/src/astro/generate.ts +15 -5
- package/src/astro/templates.ts +4 -4
- package/src/cli/required-secrets.ts +0 -1
- package/src/components/layout/PageActions.astro +119 -34
- package/src/components/layout/RootLayout.astro +18 -2
- package/src/components/layout/SiteFooter.astro +31 -8
- package/src/core/config-input.ts +38 -4
- package/src/core/data.ts +10 -3
- package/src/core/mcp-clients.ts +87 -0
- package/src/core/nav-diagnostics.ts +25 -0
- package/src/core/schema.ts +93 -3
- package/src/openapi/proxy.ts +5 -1
- package/src/ratelimit/index.ts +2 -0
- package/src/ratelimit/memory.ts +1 -1
- package/src/ratelimit/runtime.ts +81 -0
- package/src/ratelimit/schema.ts +2 -0
- package/src/ratelimit/unkey.ts +61 -0
- package/src/registry/registry.ts +1 -1
- /package/dist/cli/{chunk-xgq4eytj.js.map → chunk-080mfch6.js.map} +0 -0
- /package/dist/cli/{chunk-br0n8n3e.js.map → chunk-1ch3ct3r.js.map} +0 -0
- /package/dist/cli/{chunk-49r370mb.js.map → chunk-2ssh60qh.js.map} +0 -0
- /package/dist/cli/{chunk-1cm0h819.js.map → chunk-34wap3ed.js.map} +0 -0
- /package/dist/cli/{chunk-fqy4n3q2.js.map → chunk-40vk8kgf.js.map} +0 -0
- /package/dist/cli/{chunk-96snx8yy.js.map → chunk-487b95qy.js.map} +0 -0
- /package/dist/cli/{chunk-kkrteb15.js.map → chunk-4zxh25hc.js.map} +0 -0
- /package/dist/cli/{chunk-cn3dy3hz.js.map → chunk-69rjf786.js.map} +0 -0
- /package/dist/cli/{chunk-t5xx74rd.js.map → chunk-9jqmq5hv.js.map} +0 -0
- /package/dist/cli/{chunk-8b489v1b.js.map → chunk-aafhwx74.js.map} +0 -0
- /package/dist/cli/{chunk-2nr29y95.js.map → chunk-b680sv30.js.map} +0 -0
- /package/dist/cli/{chunk-rkqae7kh.js.map → chunk-beb0z983.js.map} +0 -0
- /package/dist/cli/{chunk-th0pgaj5.js.map → chunk-c5bjxq8v.js.map} +0 -0
- /package/dist/cli/{chunk-wehv26ry.js.map → chunk-e1n41w9m.js.map} +0 -0
- /package/dist/cli/{chunk-frk2wdq1.js.map → chunk-etckvbrh.js.map} +0 -0
- /package/dist/cli/{chunk-ahc5gc9h.js.map → chunk-f260jdrn.js.map} +0 -0
- /package/dist/cli/{chunk-05f21r8h.js.map → chunk-fnxgtjeq.js.map} +0 -0
- /package/dist/cli/{chunk-vnbvhgmd.js.map → chunk-gxhcf2hs.js.map} +0 -0
- /package/dist/cli/{chunk-bjk964fp.js.map → chunk-h0qwdbem.js.map} +0 -0
- /package/dist/cli/{chunk-enk76qd5.js.map → chunk-hn3g2b89.js.map} +0 -0
- /package/dist/cli/{chunk-w6e455pz.js.map → chunk-j4nfgn2j.js.map} +0 -0
- /package/dist/cli/{chunk-p8zg3a2h.js.map → chunk-kmswjxj9.js.map} +0 -0
- /package/dist/cli/{chunk-xt0qvz1t.js.map → chunk-nqna8dzp.js.map} +0 -0
- /package/dist/cli/{chunk-dyk3r7jw.js.map → chunk-p834rkcn.js.map} +0 -0
- /package/dist/cli/{chunk-dpcahcrb.js.map → chunk-ppa6mg4d.js.map} +0 -0
- /package/dist/cli/{chunk-79bzzkx4.js.map → chunk-q1wwrxzw.js.map} +0 -0
- /package/dist/cli/{chunk-m9bp2hc1.js.map → chunk-qcyxkj44.js.map} +0 -0
- /package/dist/cli/{chunk-a3ydrqny.js.map → chunk-rtvgmg2j.js.map} +0 -0
- /package/dist/cli/{chunk-c69tw29j.js.map → chunk-t2h8pwy1.js.map} +0 -0
- /package/dist/cli/{chunk-vzy9rh6y.js.map → chunk-x60e15st.js.map} +0 -0
- /package/dist/cli/{chunk-mrvh4c88.js.map → chunk-x7bh6d9j.js.map} +0 -0
- /package/dist/cli/{chunk-7tekknev.js.map → chunk-ydqzkhjn.js.map} +0 -0
- /package/dist/cli/{chunk-gprwej7h.js.map → chunk-yg8ckc3g.js.map} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -725,24 +725,24 @@ var main = defineCommand({
|
|
|
725
725
|
version: getBlumeVersion()
|
|
726
726
|
},
|
|
727
727
|
subCommands: {
|
|
728
|
-
add: lazyCommand(commandMeta.add, () => import("./chunk-
|
|
729
|
-
audit: lazyCommand(commandMeta.audit, () => import("./chunk-
|
|
730
|
-
build: lazyCommand(commandMeta.build, () => import("./chunk-
|
|
731
|
-
check: lazyCommand(commandMeta.check, () => import("./chunk-
|
|
732
|
-
dev: lazyCommand(commandMeta.dev, () => import("./chunk-
|
|
733
|
-
doctor: lazyCommand(commandMeta.doctor, () => import("./chunk-
|
|
734
|
-
eject: lazyCommand(commandMeta.eject, () => import("./chunk-
|
|
735
|
-
eval: lazyCommand(commandMeta.eval, () => import("./chunk-
|
|
736
|
-
init: lazyCommand(commandMeta.init, () => import("./chunk-
|
|
737
|
-
"mcp-stdio": lazyCommand(commandMeta["mcp-stdio"], () => import("./chunk-
|
|
738
|
-
migrate: lazyCommand(commandMeta.migrate, () => import("./chunk-
|
|
739
|
-
preview: lazyCommand(commandMeta.preview, () => import("./chunk-
|
|
740
|
-
skill: lazyCommand(commandMeta.skill, () => import("./chunk-
|
|
741
|
-
sync: lazyCommand(commandMeta.sync, () => import("./chunk-
|
|
742
|
-
translate: lazyCommand(commandMeta.translate, () => import("./chunk-
|
|
743
|
-
upgrade: lazyCommand(commandMeta.upgrade, () => import("./chunk-
|
|
744
|
-
validate: lazyCommand(commandMeta.validate, () => import("./chunk-
|
|
745
|
-
version: lazyCommand(commandMeta.version, () => import("./chunk-
|
|
728
|
+
add: lazyCommand(commandMeta.add, () => import("./chunk-c5bjxq8v.js"), "addCommand"),
|
|
729
|
+
audit: lazyCommand(commandMeta.audit, () => import("./chunk-2ssh60qh.js"), "auditCommand"),
|
|
730
|
+
build: lazyCommand(commandMeta.build, () => import("./chunk-nqna8dzp.js"), "buildCommand"),
|
|
731
|
+
check: lazyCommand(commandMeta.check, () => import("./chunk-beb0z983.js"), "checkCommand"),
|
|
732
|
+
dev: lazyCommand(commandMeta.dev, () => import("./chunk-x60e15st.js"), "devCommand"),
|
|
733
|
+
doctor: lazyCommand(commandMeta.doctor, () => import("./chunk-hn3g2b89.js"), "doctorCommand"),
|
|
734
|
+
eject: lazyCommand(commandMeta.eject, () => import("./chunk-1ch3ct3r.js"), "ejectCommand"),
|
|
735
|
+
eval: lazyCommand(commandMeta.eval, () => import("./chunk-9jqmq5hv.js"), "evalCommand"),
|
|
736
|
+
init: lazyCommand(commandMeta.init, () => import("./chunk-etckvbrh.js"), "initCommand"),
|
|
737
|
+
"mcp-stdio": lazyCommand(commandMeta["mcp-stdio"], () => import("./chunk-qcyxkj44.js"), "mcpStdioCommand"),
|
|
738
|
+
migrate: lazyCommand(commandMeta.migrate, () => import("./chunk-yg8ckc3g.js"), "migrateCommand"),
|
|
739
|
+
preview: lazyCommand(commandMeta.preview, () => import("./chunk-69rjf786.js"), "previewCommand"),
|
|
740
|
+
skill: lazyCommand(commandMeta.skill, () => import("./chunk-p834rkcn.js"), "skillCommand"),
|
|
741
|
+
sync: lazyCommand(commandMeta.sync, () => import("./chunk-080mfch6.js"), "syncCommand"),
|
|
742
|
+
translate: lazyCommand(commandMeta.translate, () => import("./chunk-f260jdrn.js"), "translateCommand"),
|
|
743
|
+
upgrade: lazyCommand(commandMeta.upgrade, () => import("./chunk-ydqzkhjn.js"), "upgradeCommand"),
|
|
744
|
+
validate: lazyCommand(commandMeta.validate, () => import("./chunk-ppa6mg4d.js"), "validateCommand"),
|
|
745
|
+
version: lazyCommand(commandMeta.version, () => import("./chunk-x7bh6d9j.js"), "versionCommand")
|
|
746
746
|
}
|
|
747
747
|
});
|
|
748
748
|
loadEnvFiles(process.cwd());
|
|
@@ -13,7 +13,7 @@ import type { AnySearchAdapter } from "../search/adapters/registry.ts";
|
|
|
13
13
|
import type { SourceAdapterInput } from "../sources/registry.ts";
|
|
14
14
|
import type { FontSlug } from "../theme/fonts.ts";
|
|
15
15
|
import type { FooterSocial } from "./footer.ts";
|
|
16
|
-
import type { OpenInChatProvider, SidebarDisplay, SidebarItemConfig } from "./schema.ts";
|
|
16
|
+
import type { McpClient, McpCustomClient, OpenInChatProvider, SidebarDisplay, SidebarItemConfig } from "./schema.ts";
|
|
17
17
|
import type { StandardSchema } from "./standard-schema.ts";
|
|
18
18
|
/**
|
|
19
19
|
* The public, hand-documented authoring type for `blume.config.ts`.
|
|
@@ -115,9 +115,15 @@ export interface FooterLink {
|
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* The site footer, below the content on every page: a row of links on one
|
|
118
|
-
* side,
|
|
118
|
+
* side, social profile icons on the other, and a copyright line centered
|
|
119
|
+
* between them.
|
|
119
120
|
*/
|
|
120
121
|
export interface FooterConfig {
|
|
122
|
+
/**
|
|
123
|
+
* A line of plain text centered between the links and the icons, like
|
|
124
|
+
* `© 2026 Acme, Inc.`, or a map of locale code to text.
|
|
125
|
+
*/
|
|
126
|
+
copyright?: LocalizableLabel;
|
|
121
127
|
/** Links in one row, left to right. */
|
|
122
128
|
links?: FooterLink[];
|
|
123
129
|
/**
|
|
@@ -638,6 +644,32 @@ export interface LlmsTxtConfig {
|
|
|
638
644
|
}
|
|
639
645
|
/** Expose the docs as an MCP server for connecting agents. */
|
|
640
646
|
export interface McpConfig {
|
|
647
|
+
/**
|
|
648
|
+
* The clients the "Connect to MCP" page action installs the server into,
|
|
649
|
+
* beside its "Copy server URL" row. Defaults to `true` (every built-in
|
|
650
|
+
* client). Set `false` to list none, or list built-in keys and your own
|
|
651
|
+
* clients, in order. A custom client's `command` is copied with `{name}`
|
|
652
|
+
* (the server name as an id) and `{url}` (the server's URL) filled in; its
|
|
653
|
+
* `label` localizes like a header link's, and `icon` defaults to `terminal`.
|
|
654
|
+
*
|
|
655
|
+
* ```ts
|
|
656
|
+
* agents: {
|
|
657
|
+
* mcp: {
|
|
658
|
+
* enabled: true,
|
|
659
|
+
* clients: [
|
|
660
|
+
* "claude-code",
|
|
661
|
+
* {
|
|
662
|
+
* label: "Copy Copilot CLI command",
|
|
663
|
+
* command: "copilot mcp add --transport http {name} {url}",
|
|
664
|
+
* icon: "terminal",
|
|
665
|
+
* },
|
|
666
|
+
* "vscode",
|
|
667
|
+
* ],
|
|
668
|
+
* },
|
|
669
|
+
* }
|
|
670
|
+
* ```
|
|
671
|
+
*/
|
|
672
|
+
clients?: boolean | (McpClient | McpCustomClient)[];
|
|
641
673
|
/** Turn the MCP server on. Defaults to `false`. */
|
|
642
674
|
enabled?: boolean;
|
|
643
675
|
/** Optional system hint passed to connecting agents. */
|
|
@@ -1408,8 +1440,8 @@ export interface BlumeConfig {
|
|
|
1408
1440
|
*/
|
|
1409
1441
|
feedback?: boolean | FeedbackConfig;
|
|
1410
1442
|
/**
|
|
1411
|
-
* The site footer:
|
|
1412
|
-
* the site has no footer.
|
|
1443
|
+
* The site footer: a copyright line, a row of links, and social profile
|
|
1444
|
+
* icons. Unset, the site has no footer.
|
|
1413
1445
|
*/
|
|
1414
1446
|
footer?: FooterConfig;
|
|
1415
1447
|
/** Opt-in custom frontmatter keys, validated by schemas you supply. */
|
|
@@ -1438,7 +1470,7 @@ export interface BlumeConfig {
|
|
|
1438
1470
|
* Limit how often one reader (by IP address) can call the server routes —
|
|
1439
1471
|
* the assistant, the API playground proxy, server-side search — with an
|
|
1440
1472
|
* adapter from `blume/ratelimit`: `memory()` (the default, 30 requests per
|
|
1441
|
-
* 10 minutes), `upstash()`, or `
|
|
1473
|
+
* 10 minutes), `upstash()`, `cloudflare()`, or `unkey()`. `false` turns it off.
|
|
1442
1474
|
*/
|
|
1443
1475
|
rateLimit?: false | RateLimitAdapter;
|
|
1444
1476
|
/** React island behavior (compiler auto-memoization). */
|
|
@@ -193,7 +193,7 @@ export interface BlumeDataConfig {
|
|
|
193
193
|
feedback: boolean;
|
|
194
194
|
/** Whether the rating asks for a written comment after it (`feedback.comments`). */
|
|
195
195
|
feedbackComments: boolean;
|
|
196
|
-
/** `footer`:
|
|
196
|
+
/** `footer`: the copyright line, links, and social profile icons, or `null` when unset. */
|
|
197
197
|
footer: ResolvedConfig["footer"] | null;
|
|
198
198
|
/**
|
|
199
199
|
* Repo coordinates for content components that address the API or build
|
|
@@ -217,8 +217,12 @@ export interface BlumeDataConfig {
|
|
|
217
217
|
/** `markdown.imageZoom`: click-to-zoom content images. */
|
|
218
218
|
imageZoom: boolean;
|
|
219
219
|
logo: BlumeLogo | null;
|
|
220
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* Hosted MCP server, or `null` when MCP is off, with the clients its
|
|
222
|
+
* "Connect to MCP" page action lists (`agents.mcp.clients`), in order.
|
|
223
|
+
*/
|
|
221
224
|
mcp: {
|
|
225
|
+
clients: ResolvedConfig["agents"]["mcp"]["clients"];
|
|
222
226
|
name: string;
|
|
223
227
|
route: string;
|
|
224
228
|
} | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { LocalizableLabel } from "./schema.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Clients the "Connect to MCP" page action knows how to install the server
|
|
4
|
+
* into, in display order. Shared by the config schema (`agents.mcp.clients`
|
|
5
|
+
* entries validate against this list) and the PageActions menu (which renders
|
|
6
|
+
* the full list when the config is `true`). Lives outside `schema.ts` so the
|
|
7
|
+
* component can import the list without pulling the whole config schema into
|
|
8
|
+
* the layout module graph.
|
|
9
|
+
*/
|
|
10
|
+
export declare const mcpClients: readonly ["claude-code", "codex", "cursor", "vscode"];
|
|
11
|
+
export type McpClient = (typeof mcpClients)[number];
|
|
12
|
+
/** A client Blume doesn't ship: a menu row that copies its `command`. */
|
|
13
|
+
export interface McpCustomClient {
|
|
14
|
+
/**
|
|
15
|
+
* Command the row copies, with `{name}` (the server name as an id, like
|
|
16
|
+
* `acme-docs`) and `{url}` (the server's absolute URL) filled in.
|
|
17
|
+
*/
|
|
18
|
+
command: string;
|
|
19
|
+
/** Icon beside the label: a Lucide name, inline SVG, or image. Defaults to `terminal`. */
|
|
20
|
+
icon?: string;
|
|
21
|
+
/** Row label, optionally per locale (`{ en: "…", de: "…" }`). */
|
|
22
|
+
label: LocalizableLabel;
|
|
23
|
+
}
|
|
24
|
+
export type McpClientEntry = McpClient | McpCustomClient;
|
|
25
|
+
/** A menu entry as a page renders it: a custom label resolved to its locale. */
|
|
26
|
+
export type McpMenuEntry = McpClient | (Omit<McpCustomClient, "label"> & {
|
|
27
|
+
label: string;
|
|
28
|
+
});
|
|
29
|
+
/** Whether a `clients` entry names a built-in client rather than a custom one. */
|
|
30
|
+
export declare const isBuiltInMcpClient: (client: McpClientEntry | McpMenuEntry) => client is McpClient;
|
|
31
|
+
/** Resolve the custom clients' labels for one page's locale. */
|
|
32
|
+
export declare const mcpMenuEntries: (clients: readonly McpClientEntry[], locale?: string, defaultLocale?: string) => McpMenuEntry[];
|
|
33
|
+
/**
|
|
34
|
+
* The menu's rows after "Copy server URL", each with whether a rule goes
|
|
35
|
+
* above it: wherever the list switches between commands to copy and install
|
|
36
|
+
* links ("Copy server URL" copies too), so the default menu keeps its one
|
|
37
|
+
* rule before Cursor.
|
|
38
|
+
*/
|
|
39
|
+
export declare const mcpMenuRows: (entries: readonly McpMenuEntry[]) => {
|
|
40
|
+
client: McpMenuEntry;
|
|
41
|
+
rule: boolean;
|
|
42
|
+
}[];
|
|
43
|
+
/** Fill a custom client's command for the server it installs. */
|
|
44
|
+
export declare const mcpClientCommand: (command: string, server: {
|
|
45
|
+
name: string;
|
|
46
|
+
url: string;
|
|
47
|
+
}) => string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { McpClientEntry } from "./mcp-clients.ts";
|
|
1
2
|
import type { SidebarItemConfig } from "./schema.ts";
|
|
2
3
|
import type { Diagnostic, Navigation, PageRecord } from "./types.ts";
|
|
3
4
|
/** Warn about icon names that aren't in Blume's set (skipping image/SVG icons). */
|
|
@@ -10,6 +11,12 @@ export declare const validateSearchPopularIcons: (popular: {
|
|
|
10
11
|
icon?: string;
|
|
11
12
|
label: string;
|
|
12
13
|
}[]) => Diagnostic[];
|
|
14
|
+
/**
|
|
15
|
+
* Warn about unknown icons on custom "Connect to MCP" clients
|
|
16
|
+
* (`agents.mcp.clients`), which live outside the navigation model too. A
|
|
17
|
+
* per-locale label is named in the default locale.
|
|
18
|
+
*/
|
|
19
|
+
export declare const validateMcpClientIcons: (clients: readonly McpClientEntry[], defaultLocale?: string) => Diagnostic[];
|
|
13
20
|
/**
|
|
14
21
|
* Warn when a config-linked tab/selector target has no matching page. `routes`
|
|
15
22
|
* must be the full set of servable routes — content, custom `.astro` pages, and
|
|
@@ -994,19 +994,21 @@ declare const aiConfigSchema: z.ZodObject<{
|
|
|
994
994
|
tools: z.ZodOptional<z.ZodBoolean>;
|
|
995
995
|
}, z.core.$strict>>;
|
|
996
996
|
openInChat: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodArray<z.ZodEnum<{
|
|
997
|
+
cursor: "cursor";
|
|
997
998
|
v0: "v0";
|
|
998
999
|
chatgpt: "chatgpt";
|
|
999
1000
|
claude: "claude";
|
|
1000
1001
|
t3: "t3";
|
|
1001
1002
|
scira: "scira";
|
|
1002
|
-
|
|
1003
|
-
}>>]>>, z.ZodTransform<("v0" | "chatgpt" | "claude" | "t3" | "scira" | "cursor")[], boolean | ("v0" | "chatgpt" | "claude" | "t3" | "scira" | "cursor")[]>>;
|
|
1003
|
+
}>>]>>, z.ZodTransform<("cursor" | "v0" | "chatgpt" | "claude" | "t3" | "scira")[], boolean | ("cursor" | "v0" | "chatgpt" | "claude" | "t3" | "scira")[]>>;
|
|
1004
1004
|
}, z.core.$strict>;
|
|
1005
1005
|
export { assistantReasoningLevels } from "../ai/ask.ts";
|
|
1006
1006
|
export type { AssistantReasoning } from "../ai/ask.ts";
|
|
1007
1007
|
export type AssistantConfig = NonNullable<z.infer<typeof aiConfigSchema>["assistant"]>;
|
|
1008
1008
|
export { openInChatProviders } from "./open-in-chat.ts";
|
|
1009
1009
|
export type { OpenInChatProvider } from "./open-in-chat.ts";
|
|
1010
|
+
export { mcpClients } from "./mcp-clients.ts";
|
|
1011
|
+
export type { McpClient, McpCustomClient } from "./mcp-clients.ts";
|
|
1010
1012
|
/** A configured locale: ISO-ish code plus display metadata for the switcher. */
|
|
1011
1013
|
declare const localeSchema: z.ZodObject<{
|
|
1012
1014
|
code: z.ZodString;
|
|
@@ -1206,6 +1208,24 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1206
1208
|
}>>;
|
|
1207
1209
|
markdownComponents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodCustom<ComponentMarkdown, ComponentMarkdown>>>;
|
|
1208
1210
|
mcp: z.ZodPrefault<z.ZodObject<{
|
|
1211
|
+
clients: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodArray<z.ZodUnion<readonly [z.ZodEnum<{
|
|
1212
|
+
"claude-code": "claude-code";
|
|
1213
|
+
codex: "codex";
|
|
1214
|
+
cursor: "cursor";
|
|
1215
|
+
vscode: "vscode";
|
|
1216
|
+
}>, z.ZodObject<{
|
|
1217
|
+
command: z.ZodString;
|
|
1218
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
1219
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
1220
|
+
}, z.core.$strict>]>>]>>, z.ZodTransform<("claude-code" | "codex" | "cursor" | "vscode" | {
|
|
1221
|
+
command: string;
|
|
1222
|
+
label: string | Record<string, string>;
|
|
1223
|
+
icon?: string | undefined;
|
|
1224
|
+
})[], boolean | ("claude-code" | "codex" | "cursor" | "vscode" | {
|
|
1225
|
+
command: string;
|
|
1226
|
+
label: string | Record<string, string>;
|
|
1227
|
+
icon?: string | undefined;
|
|
1228
|
+
})[]>>;
|
|
1209
1229
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
1210
1230
|
instructions: z.ZodOptional<z.ZodString>;
|
|
1211
1231
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -1393,13 +1413,13 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1393
1413
|
tools: z.ZodOptional<z.ZodBoolean>;
|
|
1394
1414
|
}, z.core.$strict>>;
|
|
1395
1415
|
openInChat: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodArray<z.ZodEnum<{
|
|
1416
|
+
cursor: "cursor";
|
|
1396
1417
|
v0: "v0";
|
|
1397
1418
|
chatgpt: "chatgpt";
|
|
1398
1419
|
claude: "claude";
|
|
1399
1420
|
t3: "t3";
|
|
1400
1421
|
scira: "scira";
|
|
1401
|
-
|
|
1402
|
-
}>>]>>, z.ZodTransform<("v0" | "chatgpt" | "claude" | "t3" | "scira" | "cursor")[], boolean | ("v0" | "chatgpt" | "claude" | "t3" | "scira" | "cursor")[]>>;
|
|
1422
|
+
}>>]>>, z.ZodTransform<("cursor" | "v0" | "chatgpt" | "claude" | "t3" | "scira")[], boolean | ("cursor" | "v0" | "chatgpt" | "claude" | "t3" | "scira")[]>>;
|
|
1403
1423
|
}, z.core.$strict>>;
|
|
1404
1424
|
analytics: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1405
1425
|
kind: z.ZodLiteral<"adobe">;
|
|
@@ -3038,6 +3058,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3038
3058
|
enabled: boolean;
|
|
3039
3059
|
}>>>;
|
|
3040
3060
|
footer: z.ZodOptional<z.ZodObject<{
|
|
3061
|
+
copyright: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
3041
3062
|
links: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
3042
3063
|
href: z.ZodString;
|
|
3043
3064
|
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
@@ -3350,6 +3371,16 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3350
3371
|
}, z.core.$strict>;
|
|
3351
3372
|
requiredSecrets: z.ZodArray<z.ZodString>;
|
|
3352
3373
|
runtimeDeps: z.ZodArray<z.ZodString>;
|
|
3374
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3375
|
+
kind: z.ZodLiteral<"unkey">;
|
|
3376
|
+
options: z.ZodObject<{
|
|
3377
|
+
requests: z.ZodOptional<z.ZodNumber>;
|
|
3378
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
3379
|
+
rootKeyEnv: z.ZodOptional<z.ZodString>;
|
|
3380
|
+
window: z.ZodOptional<z.ZodNumber>;
|
|
3381
|
+
}, z.core.$strict>;
|
|
3382
|
+
requiredSecrets: z.ZodArray<z.ZodString>;
|
|
3383
|
+
runtimeDeps: z.ZodArray<z.ZodString>;
|
|
3353
3384
|
}, z.core.$strict>], "kind">]>>, z.ZodTransform<{
|
|
3354
3385
|
kind: "cloudflare";
|
|
3355
3386
|
options: {
|
|
@@ -3377,6 +3408,16 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3377
3408
|
};
|
|
3378
3409
|
requiredSecrets: string[];
|
|
3379
3410
|
runtimeDeps: string[];
|
|
3411
|
+
} | {
|
|
3412
|
+
kind: "unkey";
|
|
3413
|
+
options: {
|
|
3414
|
+
requests?: number | undefined;
|
|
3415
|
+
namespace?: string | undefined;
|
|
3416
|
+
rootKeyEnv?: string | undefined;
|
|
3417
|
+
window?: number | undefined;
|
|
3418
|
+
};
|
|
3419
|
+
requiredSecrets: string[];
|
|
3420
|
+
runtimeDeps: string[];
|
|
3380
3421
|
} | null, false | {
|
|
3381
3422
|
kind: "cloudflare";
|
|
3382
3423
|
options: {
|
|
@@ -3404,6 +3445,16 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3404
3445
|
};
|
|
3405
3446
|
requiredSecrets: string[];
|
|
3406
3447
|
runtimeDeps: string[];
|
|
3448
|
+
} | {
|
|
3449
|
+
kind: "unkey";
|
|
3450
|
+
options: {
|
|
3451
|
+
requests?: number | undefined;
|
|
3452
|
+
namespace?: string | undefined;
|
|
3453
|
+
rootKeyEnv?: string | undefined;
|
|
3454
|
+
window?: number | undefined;
|
|
3455
|
+
};
|
|
3456
|
+
requiredSecrets: string[];
|
|
3457
|
+
runtimeDeps: string[];
|
|
3407
3458
|
} | undefined>>;
|
|
3408
3459
|
react: z.ZodPrefault<z.ZodObject<{
|
|
3409
3460
|
compiler: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -25,3 +25,5 @@ export type { MemoryAdapter, RateLimitOptions } from "./memory.ts";
|
|
|
25
25
|
export type { RateLimitAdapter } from "./schema.ts";
|
|
26
26
|
export { upstash } from "./upstash.ts";
|
|
27
27
|
export type { UpstashAdapter, UpstashOptions } from "./upstash.ts";
|
|
28
|
+
export { unkey } from "./unkey.ts";
|
|
29
|
+
export type { UnkeyAdapter, UnkeyOptions } from "./unkey.ts";
|
|
@@ -31,6 +31,6 @@ export declare const memoryAdapterSchema: z.ZodObject<{
|
|
|
31
31
|
* platform that runs many short-lived instances (Vercel, Netlify,
|
|
32
32
|
* Cloudflare) each instance keeps its own, so it stops a burst from one
|
|
33
33
|
* reader rather than enforcing the limit exactly. For that, share the count
|
|
34
|
-
* with `upstash()` or `
|
|
34
|
+
* with `upstash()`, `cloudflare()`, or `unkey()`.
|
|
35
35
|
*/
|
|
36
36
|
export declare const memory: (options?: RateLimitOptions) => MemoryAdapter;
|
|
@@ -27,6 +27,16 @@ export declare const rateLimitAdapterSchema: z.ZodDiscriminatedUnion<[z.ZodObjec
|
|
|
27
27
|
}, z.core.$strict>;
|
|
28
28
|
requiredSecrets: z.ZodArray<z.ZodString>;
|
|
29
29
|
runtimeDeps: z.ZodArray<z.ZodString>;
|
|
30
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
31
|
+
kind: z.ZodLiteral<"unkey">;
|
|
32
|
+
options: z.ZodObject<{
|
|
33
|
+
requests: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
35
|
+
rootKeyEnv: z.ZodOptional<z.ZodString>;
|
|
36
|
+
window: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
}, z.core.$strict>;
|
|
38
|
+
requiredSecrets: z.ZodArray<z.ZodString>;
|
|
39
|
+
runtimeDeps: z.ZodArray<z.ZodString>;
|
|
30
40
|
}, z.core.$strict>], "kind">;
|
|
31
41
|
export type RateLimitAdapter = z.output<typeof rateLimitAdapterSchema>;
|
|
32
42
|
/**
|
|
@@ -62,6 +72,16 @@ export declare const rateLimitConfigSchema: z.ZodPipe<z.ZodOptional<z.ZodUnion<r
|
|
|
62
72
|
}, z.core.$strict>;
|
|
63
73
|
requiredSecrets: z.ZodArray<z.ZodString>;
|
|
64
74
|
runtimeDeps: z.ZodArray<z.ZodString>;
|
|
75
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
76
|
+
kind: z.ZodLiteral<"unkey">;
|
|
77
|
+
options: z.ZodObject<{
|
|
78
|
+
requests: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
80
|
+
rootKeyEnv: z.ZodOptional<z.ZodString>;
|
|
81
|
+
window: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
}, z.core.$strict>;
|
|
83
|
+
requiredSecrets: z.ZodArray<z.ZodString>;
|
|
84
|
+
runtimeDeps: z.ZodArray<z.ZodString>;
|
|
65
85
|
}, z.core.$strict>], "kind">]>>, z.ZodTransform<{
|
|
66
86
|
kind: "cloudflare";
|
|
67
87
|
options: {
|
|
@@ -89,6 +109,16 @@ export declare const rateLimitConfigSchema: z.ZodPipe<z.ZodOptional<z.ZodUnion<r
|
|
|
89
109
|
};
|
|
90
110
|
requiredSecrets: string[];
|
|
91
111
|
runtimeDeps: string[];
|
|
112
|
+
} | {
|
|
113
|
+
kind: "unkey";
|
|
114
|
+
options: {
|
|
115
|
+
requests?: number | undefined;
|
|
116
|
+
namespace?: string | undefined;
|
|
117
|
+
rootKeyEnv?: string | undefined;
|
|
118
|
+
window?: number | undefined;
|
|
119
|
+
};
|
|
120
|
+
requiredSecrets: string[];
|
|
121
|
+
runtimeDeps: string[];
|
|
92
122
|
} | null, false | {
|
|
93
123
|
kind: "cloudflare";
|
|
94
124
|
options: {
|
|
@@ -116,4 +146,14 @@ export declare const rateLimitConfigSchema: z.ZodPipe<z.ZodOptional<z.ZodUnion<r
|
|
|
116
146
|
};
|
|
117
147
|
requiredSecrets: string[];
|
|
118
148
|
runtimeDeps: string[];
|
|
149
|
+
} | {
|
|
150
|
+
kind: "unkey";
|
|
151
|
+
options: {
|
|
152
|
+
requests?: number | undefined;
|
|
153
|
+
namespace?: string | undefined;
|
|
154
|
+
rootKeyEnv?: string | undefined;
|
|
155
|
+
window?: number | undefined;
|
|
156
|
+
};
|
|
157
|
+
requiredSecrets: string[];
|
|
158
|
+
runtimeDeps: string[];
|
|
119
159
|
} | undefined>>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { AdapterDescriptor } from "../core/adapter.ts";
|
|
3
|
+
import type { RateLimitOptions } from "./memory.ts";
|
|
4
|
+
/** The Unkey rate limit namespace the count is kept in, unless set. */
|
|
5
|
+
export declare const UNKEY_NAMESPACE = "docs";
|
|
6
|
+
/** The longest window Unkey takes, in seconds (30 days). */
|
|
7
|
+
export declare const UNKEY_MAX_WINDOW = 2592000;
|
|
8
|
+
/** Options for {@link unkey}. */
|
|
9
|
+
export interface UnkeyOptions extends RateLimitOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The Unkey rate limit namespace, up to 512 characters. Defaults to `docs`;
|
|
12
|
+
* Unkey creates it on the first request.
|
|
13
|
+
*/
|
|
14
|
+
namespace?: string;
|
|
15
|
+
/** Name of the env var holding the root key. Defaults to `UNKEY_ROOT_KEY`. */
|
|
16
|
+
rootKeyEnv?: string;
|
|
17
|
+
/** The window, in seconds: Unkey takes up to 30 days. */
|
|
18
|
+
window?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const unkeyOptionsSchema: z.ZodObject<{
|
|
21
|
+
requests: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
23
|
+
rootKeyEnv: z.ZodOptional<z.ZodString>;
|
|
24
|
+
window: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
export type UnkeyAdapter = AdapterDescriptor<"unkey", UnkeyOptions>;
|
|
27
|
+
export declare const unkeyAdapterSchema: z.ZodObject<{
|
|
28
|
+
kind: z.ZodLiteral<"unkey">;
|
|
29
|
+
options: z.ZodObject<{
|
|
30
|
+
requests: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
32
|
+
rootKeyEnv: z.ZodOptional<z.ZodString>;
|
|
33
|
+
window: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
}, z.core.$strict>;
|
|
35
|
+
requiredSecrets: z.ZodArray<z.ZodString>;
|
|
36
|
+
runtimeDeps: z.ZodArray<z.ZodString>;
|
|
37
|
+
}, z.core.$strict>;
|
|
38
|
+
/** The env var the root key is read from. */
|
|
39
|
+
export declare const unkeySecrets: (options: UnkeyOptions) => [string];
|
|
40
|
+
/**
|
|
41
|
+
* Counts with Unkey's rate limiting, which every instance on every host
|
|
42
|
+
* shares. Unkey counts in each region and shares the counts between them
|
|
43
|
+
* within moments, so a short burst split across regions can pass more than
|
|
44
|
+
* the limit. It talks to Unkey's REST API, so there is nothing to install;
|
|
45
|
+
* the root key comes from `UNKEY_ROOT_KEY`, or the env var `rootKeyEnv`
|
|
46
|
+
* names.
|
|
47
|
+
*/
|
|
48
|
+
export declare const unkey: (options?: UnkeyOptions) => UnkeyAdapter;
|
package/docs/02-deployment.mdx
CHANGED
|
@@ -267,6 +267,7 @@ When a feature needs a secret, Blume warns at `blume dev`/`build` if it's missin
|
|
|
267
267
|
| Assistant (other adapters) | the adapter's default key env var (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, `OPENROUTER_API_KEY`, `LLMGATEWAY_API_KEY`, `INKEEP_API_KEY`), or the `apiKeyEnv` you passed it |
|
|
268
268
|
| [Assistant bot protection](/docs/configuration/assistant#bot-protection) | `TURNSTILE_SECRET_KEY` (`turnstile()`), `HCAPTCHA_SECRET_KEY` (`hcaptcha()`) |
|
|
269
269
|
| [Rate limiting](/docs/configuration/rate-limiting) (`upstash()`) | `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` |
|
|
270
|
+
| [Rate limiting](/docs/configuration/rate-limiting) (`unkey()`) | `UNKEY_ROOT_KEY`, or the `rootKeyEnv` you passed it |
|
|
270
271
|
| Mixedbread search | `MIXEDBREAD_API_KEY` |
|
|
271
272
|
| [Narration](/docs/configuration/narration) (generated voices) | `AI_GATEWAY_API_KEY` (or Vercel OIDC) for `gateway()`, `OPENAI_API_KEY` for `openai()` (none with a `baseUrl`, unless you name one), or the `apiKeyEnv` you passed to either |
|
|
272
273
|
| [Content sources](/docs/content/sources) | `NOTION_TOKEN` (`notion()`), `SANITY_TOKEN` (`sanity()`), `CONTENTFUL_ACCESS_TOKEN` (`contentful()`, plus `CONTENTFUL_PREVIEW_TOKEN` for `--preview`, which fails without it), `PAYLOAD_API_KEY` (`payload()`), `STRAPI_API_TOKEN` (`strapi()`), `GITHUB_TOKEN` (`githubReleases()`, and `mdxRemote()` with `github`) |
|
|
@@ -101,7 +101,7 @@ export default defineConfig({
|
|
|
101
101
|
| `description` | — | Default meta description, used for SEO and OG. |
|
|
102
102
|
| `logo` | — | Brand mark and/or wordmark shown in the header. |
|
|
103
103
|
| `banner` | — | Site-wide announcement bar above the header. |
|
|
104
|
-
| `footer` | — | A row of links and social profile icons below the content. |
|
|
104
|
+
| `footer` | — | A copyright line, a row of links, and social profile icons below the content. |
|
|
105
105
|
|
|
106
106
|
### Logo
|
|
107
107
|
|
|
@@ -204,10 +204,11 @@ A translated banner keys dismissal on the default locale's text, so one dismissa
|
|
|
204
204
|
|
|
205
205
|
### Footer
|
|
206
206
|
|
|
207
|
-
Every page ends with a footer: one row, as tall as the header, with your links on one side
|
|
207
|
+
Every page ends with a footer: one row, as tall as the header, with your links on one side, social profile icons on the other, and a copyright line centered between them. The footer is the only place Blume links your social profiles and repository, so the [`github`](#github) repository comes first among the icons whenever it's set. `footer` adds the rest:
|
|
208
208
|
|
|
209
209
|
```ts blume.config.ts lineNumbers
|
|
210
210
|
footer: {
|
|
211
|
+
copyright: "© 2026 Acme, Inc.",
|
|
211
212
|
links: [
|
|
212
213
|
{ label: "Pricing", href: "https://acme.dev/pricing" },
|
|
213
214
|
{ label: "Changelog", href: "/changelog" },
|
|
@@ -220,9 +221,9 @@ footer: {
|
|
|
220
221
|
},
|
|
221
222
|
```
|
|
222
223
|
|
|
223
|
-
`links` appear in the order you write them. Write internal links as if mounted at root; an external link opens in a new tab. On a [multilingual](/docs/content/i18n#per-locale-navigation) site, a `label` can be a map of locale code to text, and an internal link moves into the reader's locale whenever that locale serves the page. `socials` maps a platform to its profile URL, and the icons appear in the order you write them. The platforms are `bluesky`, `discord`, `facebook`, `github`, `hacker-news`, `instagram`, `linkedin`, `medium`, `podcast`, `reddit`, `slack`, `telegram`, `threads`, `website`, `x`, and `youtube`. A `github` social replaces the repository link; hide that link with [`navigation.repo`](/docs/content/navigation#repository-link).
|
|
224
|
+
`copyright` is plain text, so write `©` itself rather than `©`. For a year that keeps itself current, use a template literal: `` `© ${new Date().getFullYear()} Acme, Inc.` ``. `links` appear in the order you write them. Write internal links as if mounted at root; an external link opens in a new tab. On a [multilingual](/docs/content/i18n#per-locale-navigation) site, `copyright` and a `label` can each be a map of locale code to text, and an internal link moves into the reader's locale whenever that locale serves the page. `socials` maps a platform to its profile URL, and the icons appear in the order you write them. The platforms are `bluesky`, `discord`, `facebook`, `github`, `hacker-news`, `instagram`, `linkedin`, `medium`, `podcast`, `reddit`, `slack`, `telegram`, `threads`, `website`, `x`, and `youtube`. A `github` social replaces the repository link; hide that link with [`navigation.repo`](/docs/content/navigation#repository-link).
|
|
224
225
|
|
|
225
|
-
A site with no links, profiles, or repository has no footer. Custom pages built on `PageLayout` show it too, unless they fill the layout's own `footer` slot. To replace it entirely, override the [`Footer` layout slot](/docs/configuration/customization#layout-slots).
|
|
226
|
+
A site with no copyright, links, profiles, or repository has no footer. Custom pages built on `PageLayout` show it too, unless they fill the layout's own `footer` slot. To replace it entirely, override the [`Footer` layout slot](/docs/configuration/customization#layout-slots).
|
|
226
227
|
|
|
227
228
|
## Content
|
|
228
229
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Rate limiting
|
|
3
|
-
description: Limit how often one reader can call the assistant, the API playground proxy, and server-side search, on by default, with the count kept in memory, Upstash, or
|
|
3
|
+
description: Limit how often one reader can call the assistant, the API playground proxy, and server-side search, on by default, with the count kept in memory, Upstash, Cloudflare, or Unkey.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
The server routes a reader can call cost you something each time: the [assistant](/docs/configuration/assistant) spends model tokens, the [API playground](/docs/references/openapi#cors-and-the-proxy) proxy relays requests to your API, and [Mixedbread search](/docs/configuration/search#mixedbread) runs a paid query. Blume limits how often one reader, identified by IP address, can call each of them. A reader over the limit gets `429 Too Many Requests` with a `Retry-After` header, and the assistant tells them to try again in a few minutes.
|
|
@@ -25,6 +25,7 @@ Each route keeps its own count, so a busy playground session doesn't use up the
|
|
|
25
25
|
| `memory()` | The server's memory (the default) | Exactly on one server; per instance on serverless hosts |
|
|
26
26
|
| `upstash()` | [Upstash Redis](https://upstash.com) | Exactly, on every host |
|
|
27
27
|
| `cloudflare()` | Cloudflare's Workers rate limiting | Per Cloudflare location |
|
|
28
|
+
| `unkey()` | [Unkey](https://unkey.com) | On every host; regions catch up within moments |
|
|
28
29
|
|
|
29
30
|
### Memory
|
|
30
31
|
|
|
@@ -68,6 +69,24 @@ export default defineConfig({
|
|
|
68
69
|
|
|
69
70
|
The binding's namespace comes from the Worker's name, so two sites on one account don't share a count. Set `namespaceId` to choose it yourself.
|
|
70
71
|
|
|
72
|
+
### Unkey
|
|
73
|
+
|
|
74
|
+
`unkey()` keeps the count in [Unkey's rate limiting](https://www.unkey.com/docs/api-management/ratelimiting/overview), which every instance shares. Create a root key with the `ratelimit.*.limit` permission, plus `ratelimit.*.create_namespace` so Unkey can create the namespace on the first request, then set it as `UNKEY_ROOT_KEY`. Blume talks to Unkey's REST API, so there's nothing to install. Until the key is set, the routes count in memory and `blume build` warns.
|
|
75
|
+
|
|
76
|
+
```ts blume.config.ts lineNumbers
|
|
77
|
+
import { unkey } from "blume/ratelimit";
|
|
78
|
+
|
|
79
|
+
rateLimit: unkey(),
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The namespace is `docs` unless you name another with `namespace`. Name the variable that holds the key with `rootKeyEnv`:
|
|
83
|
+
|
|
84
|
+
```ts blume.config.ts lineNumbers
|
|
85
|
+
rateLimit: unkey({ namespace: "acme-docs", rootKeyEnv: "DOCS_UNKEY_ROOT_KEY" }),
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Unkey counts in each region and shares the counts between regions within moments, so a short burst split across regions can get a few more requests through than the limit. Every count fails with a key Unkey refuses, like one missing those permissions, and a request a shared store fails to count is let through, so if the server logs `Rate limiting failed`, check the key.
|
|
89
|
+
|
|
71
90
|
## Behind a reverse proxy
|
|
72
91
|
|
|
73
92
|
Vercel, Netlify, and Cloudflare tell Blume each reader's address. A `node()` server behind a reverse proxy, like nginx or Caddy, sees every request come from the proxy, so every reader shares one count and a busy site hits the limit for everyone. List the host the proxy answers for in `allowedDomains`:
|
package/docs/content/i18n.mdx
CHANGED
|
@@ -85,7 +85,7 @@ i18n: {
|
|
|
85
85
|
|
|
86
86
|
Each language gets its own sidebar, built from that locale's files — so translations can diverge in structure, ordering, or labels. Folder [`meta.ts`](/docs/content/meta) files resolve per locale, too: under the default `dir` parser, put a `meta.ts` under `fr/guides/` to order the French group independently. Until a locale has one (or a shared `meta.$.ts`), its group mirrors the [fallback](#fallbacks) locale's — that folder's `meta.ts` and the `sidebar.display` its index page sets — so pages that fall back keep the same titles, order, and collapsible groups. Under the `dot` parser translations sit next to the originals, so a folder's `meta.ts` applies to every locale. Everything else about [navigation](/docs/content/navigation) works the same, per language.
|
|
87
87
|
|
|
88
|
-
Header tabs, header links, and the footer are configured, not derived from content, so their labels localize in `blume.config.ts`. Every configured label accepts a per-locale map (`{ en: "Docs", fr: "Documentation" }`) alongside the plain-string form, falling back to the default locale's entry for locales you haven't filled in: tab and dropdown labels ([Tabs](/docs/content/navigation#tabs)), [featured links](/docs/content/navigation#featured-links), [header actions](/docs/content/navigation#header-actions) and the call to action, [footer](/docs/configuration#footer) links,
|
|
88
|
+
Header tabs, header links, and the footer are configured, not derived from content, so their labels localize in `blume.config.ts`. Every configured label accepts a per-locale map (`{ en: "Docs", fr: "Documentation" }`) alongside the plain-string form, falling back to the default locale's entry for locales you haven't filled in: tab and dropdown labels ([Tabs](/docs/content/navigation#tabs)), [featured links](/docs/content/navigation#featured-links), [header actions](/docs/content/navigation#header-actions) and the call to action, [footer](/docs/configuration#footer) links, the [banner](/docs/configuration#banner)'s text and link text, and custom clients in the [Connect to MCP](/docs/discoverability/mcp#connect-to-mcp-menu) menu.
|
|
89
89
|
|
|
90
90
|
```ts blume.config.ts lineNumbers
|
|
91
91
|
navigation: {
|