blume 1.6.0 → 1.6.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 +21 -0
- package/dist/cli/index.js +1318 -270
- package/dist/cli/index.js.map +68 -61
- package/dist/types/core/config-input.d.ts +9 -0
- package/dist/types/core/data.d.ts +12 -1
- package/dist/types/core/i18n-ui.d.ts +4 -0
- package/dist/types/core/schema.d.ts +7 -0
- package/dist/types/core/types.d.ts +6 -0
- package/dist/types/openapi/references.d.ts +5 -0
- package/docs/07-faq.mdx +9 -9
- package/docs/advanced/api-reference.mdx +10 -1
- package/docs/advanced/custom-pages.mdx +3 -1
- package/docs/advanced/graphql.mdx +1 -1
- package/docs/configuration/ai.mdx +76 -7
- package/docs/configuration/seo.mdx +3 -3
- package/docs/configuration/theming.mdx +6 -0
- package/docs/content/components.mdx +8 -1
- package/docs/index.mdx +2 -2
- package/package.json +53 -53
- package/skills/blume/SKILL.md +2 -2
- package/src/ai/agent-readability.ts +60 -17
- package/src/ai/api/handlers.ts +273 -0
- package/src/ai/api/paths.ts +14 -0
- package/src/ai/api/problem.ts +63 -0
- package/src/ai/api/spec.ts +681 -0
- package/src/ai/api-catalog.ts +11 -1
- package/src/ai/link-headers.ts +12 -3
- package/src/ai/llms.ts +9 -2
- package/src/ai/mcp/query.ts +390 -0
- package/src/ai/mcp/server.ts +32 -352
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +256 -64
- package/src/astro/index.ts +7 -0
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +398 -38
- package/src/cli/commands/build.ts +9 -1
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/PageActions.astro +25 -14
- package/src/core/config-input.ts +9 -0
- package/src/core/data.ts +17 -2
- package/src/core/define-components.ts +2 -0
- package/src/core/i18n-ui.ts +3 -0
- package/src/core/includes.ts +2 -1
- package/src/core/manifest.ts +10 -0
- package/src/core/schema.ts +20 -5
- package/src/core/types.ts +6 -0
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +1 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +5 -3
- package/src/deploy/vercel-negotiation.ts +97 -6
- package/src/og/card.ts +1 -1
- package/src/openapi/references.ts +8 -0
- package/src/openapi/render-mdx.ts +18 -4
- package/src/openapi/scalar.ts +0 -4
- package/src/registry/eject.ts +36 -17
- package/src/theme/entry.ts +2 -2
- package/src/theme/sources.ts +49 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite a bare `--host` in the raw argv to `--host=` before citty parses it.
|
|
3
|
+
*
|
|
4
|
+
* `host` is a string arg (citty has no mixed string/boolean type), and citty
|
|
5
|
+
* 0.2 parses with `node:util.parseArgs`, where a string option consumes the
|
|
6
|
+
* next token as its value even when that token is another flag: `blume dev
|
|
7
|
+
* --host --open` would bind the literal hostname "--open" and drop `--open`.
|
|
8
|
+
* The `--host=` spelling parses as `""` without touching its neighbor, which
|
|
9
|
+
* `normalizeHost` then maps to Astro's "bind all interfaces".
|
|
10
|
+
*/
|
|
11
|
+
export const normalizeHostArgs = (rawArgs: readonly string[]): string[] =>
|
|
12
|
+
rawArgs.map((arg, index) => {
|
|
13
|
+
if (arg !== "--host") {
|
|
14
|
+
return arg;
|
|
15
|
+
}
|
|
16
|
+
const next = rawArgs[index + 1];
|
|
17
|
+
return next === undefined || next.startsWith("-") ? "--host=" : arg;
|
|
18
|
+
});
|
package/src/cli/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { translateCommand } from "./commands/translate.ts";
|
|
|
17
17
|
import { validateCommand } from "./commands/validate.ts";
|
|
18
18
|
import { versionCommand } from "./commands/version.ts";
|
|
19
19
|
import { loadEnvFiles } from "./env.ts";
|
|
20
|
+
import { normalizeHostArgs } from "./host-args.ts";
|
|
20
21
|
import { reportInternalError } from "./internal-error.ts";
|
|
21
22
|
|
|
22
23
|
const main = defineCommand({
|
|
@@ -60,4 +61,4 @@ process.on("unhandledRejection", (error) => {
|
|
|
60
61
|
process.exit(1);
|
|
61
62
|
});
|
|
62
63
|
|
|
63
|
-
runMain(main);
|
|
64
|
+
runMain(main, { rawArgs: normalizeHostArgs(process.argv.slice(2)) });
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* blocks, page actions, color swatches, prompts, API panels, Ask AI). One
|
|
4
4
|
* implementation owns the invariants each site used to hand-roll:
|
|
5
5
|
*
|
|
6
|
-
* - the clipboard write is guarded
|
|
7
|
-
*
|
|
6
|
+
* - the clipboard write is guarded and a confirmation must never lie: a
|
|
7
|
+
* site either shows nothing on failure or (the page actions) shows a
|
|
8
|
+
* failure label, but never a false "Copied";
|
|
8
9
|
* - repeat copies restart the hold instead of stacking timers, so the copied
|
|
9
10
|
* state never reverts early after a double-click;
|
|
10
|
-
* - every
|
|
11
|
-
* confirmation is audible, not just visual (previously
|
|
12
|
-
* button announced).
|
|
11
|
+
* - every flash is announced to a shared polite live region, so the
|
|
12
|
+
* confirmation — or the failure — is audible, not just visual (previously
|
|
13
|
+
* only the code-block button announced).
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/** How long the copied confirmation holds before reverting. */
|
|
@@ -35,17 +36,100 @@ export const announceCopied = (message: string): void => {
|
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* The legacy copy path: select `text` in an off-screen textarea and run the
|
|
40
|
+
* `copy` editing command. It needs no clipboard permission, only the user
|
|
41
|
+
* activation the click already provides, so it covers the places the async
|
|
42
|
+
* Clipboard API doesn't reach — in-app browsers and WebViews that ship no
|
|
43
|
+
* `navigator.clipboard`, insecure origins, and a denied permission prompt.
|
|
44
|
+
*/
|
|
45
|
+
const copyViaCommand = (text: string): boolean => {
|
|
46
|
+
const previous = document.activeElement;
|
|
47
|
+
const textarea = document.createElement("textarea");
|
|
48
|
+
textarea.value = text;
|
|
49
|
+
textarea.setAttribute("readonly", "");
|
|
50
|
+
textarea.setAttribute("aria-hidden", "true");
|
|
51
|
+
// Off-screen rather than `display: none`: hidden controls can't be selected.
|
|
52
|
+
textarea.style.position = "fixed";
|
|
53
|
+
textarea.style.top = "0";
|
|
54
|
+
textarea.style.left = "-9999px";
|
|
55
|
+
textarea.style.opacity = "0";
|
|
56
|
+
// Beside the focused control, not on `<body>`: `select()` moves focus to the
|
|
57
|
+
// textarea, and a copy button inside a light-dismissed `<details>` menu (the
|
|
58
|
+
// MCP actions) would otherwise see focus leave the panel — closing the menu
|
|
59
|
+
// mid-click and hiding the label the outcome is about to flash on.
|
|
60
|
+
const host =
|
|
61
|
+
previous instanceof HTMLElement && previous.parentElement
|
|
62
|
+
? previous.parentElement
|
|
63
|
+
: document.body;
|
|
64
|
+
host.append(textarea);
|
|
65
|
+
textarea.select();
|
|
66
|
+
// iOS WebKit has been known to ignore `select()` on a readonly textarea;
|
|
67
|
+
// an explicit range covers it (the same belt-and-braces clipboard.js uses).
|
|
68
|
+
textarea.setSelectionRange(0, text.length);
|
|
69
|
+
let copied = false;
|
|
70
|
+
try {
|
|
71
|
+
copied = document.execCommand("copy");
|
|
72
|
+
} catch {
|
|
73
|
+
// Some engines throw instead of returning false; either way it failed.
|
|
74
|
+
}
|
|
75
|
+
textarea.remove();
|
|
76
|
+
// `select()` moved focus to the textarea; put it back on the button so a
|
|
77
|
+
// keyboard user isn't dropped at the top of the document.
|
|
78
|
+
if (previous instanceof HTMLElement) {
|
|
79
|
+
previous.focus();
|
|
80
|
+
}
|
|
81
|
+
return copied;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Copy `text` to the clipboard. Returns whether the write succeeded. The async
|
|
86
|
+
* Clipboard API is tried first; when it is missing (in-app browsers, insecure
|
|
87
|
+
* contexts) or rejects (a denied permission), the legacy `copy` command is
|
|
88
|
+
* tried before giving up, so callers only see `false` when nothing worked and
|
|
89
|
+
* can show a failure instead of silently doing nothing.
|
|
41
90
|
*/
|
|
42
91
|
export const copyText = async (text: string): Promise<boolean> => {
|
|
43
92
|
try {
|
|
44
93
|
await navigator.clipboard.writeText(text);
|
|
45
94
|
return true;
|
|
46
95
|
} catch {
|
|
47
|
-
return
|
|
96
|
+
return copyViaCommand(text);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Copy text that still has to be loaded — the page's Markdown mirror, fetched
|
|
102
|
+
* on click. Safari and Firefox only honor a clipboard write issued inside the
|
|
103
|
+
* click's own task: awaiting the load first lands the write outside the user
|
|
104
|
+
* activation, so `writeText` rejects and the legacy command returns `false`
|
|
105
|
+
* even though the clipboard is perfectly available. `ClipboardItem` accepts a
|
|
106
|
+
* promise for its payload, so the write is issued synchronously with the
|
|
107
|
+
* load still in flight and the activation intact. Engines without it (or a
|
|
108
|
+
* write that rejects for any reason, a failed load included) fall back to the
|
|
109
|
+
* awaited {@link copyText}, which is what Chrome's longer activation window
|
|
110
|
+
* already tolerated. A load failure still throws, so the caller can report
|
|
111
|
+
* it rather than a clipboard problem.
|
|
112
|
+
*/
|
|
113
|
+
export const copyDeferredText = async (
|
|
114
|
+
load: () => Promise<string>
|
|
115
|
+
): Promise<boolean> => {
|
|
116
|
+
const text = load();
|
|
117
|
+
if ("ClipboardItem" in globalThis) {
|
|
118
|
+
try {
|
|
119
|
+
await navigator.clipboard.write([
|
|
120
|
+
new ClipboardItem({
|
|
121
|
+
"text/plain": text.then(
|
|
122
|
+
(value) => new Blob([value], { type: "text/plain" })
|
|
123
|
+
),
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
// Fall through to the awaited write; if the load itself failed, the
|
|
129
|
+
// `await` below rethrows that error for the caller.
|
|
130
|
+
}
|
|
48
131
|
}
|
|
132
|
+
return copyText(await text);
|
|
49
133
|
};
|
|
50
134
|
|
|
51
135
|
/**
|
|
@@ -176,7 +176,7 @@ const AskAI = ({
|
|
|
176
176
|
useEffect(() => {
|
|
177
177
|
// The initial null→body flip is deliberate (there is no body during SSR);
|
|
178
178
|
// it is the same one-time post-mount cascade the old `mounted` flag had.
|
|
179
|
-
// oxlint-disable-next-line react/react-compiler -- deliberate post-mount portal-target initialization
|
|
179
|
+
// oxlint-disable-next-line react/react-compiler, react/set-state-in-effect -- deliberate post-mount portal-target initialization
|
|
180
180
|
setPortalTarget(document.body);
|
|
181
181
|
const onSwap = () => setPortalTarget(document.body);
|
|
182
182
|
document.addEventListener("astro:after-swap", onSwap);
|
|
@@ -263,6 +263,7 @@ const AskAI = ({
|
|
|
263
263
|
if (open && portalTarget) {
|
|
264
264
|
document.body.dataset.blumeAsk = "open";
|
|
265
265
|
}
|
|
266
|
+
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- see above
|
|
266
267
|
}, [open, portalTarget]);
|
|
267
268
|
|
|
268
269
|
// Below the desktop dock breakpoint the open panel is a full-width overlay,
|
|
@@ -328,12 +329,14 @@ const AskAI = ({
|
|
|
328
329
|
// portalTarget: each swap installs a new <body>, so the sweep and its
|
|
329
330
|
// observer must re-run against the new children (the old ones are
|
|
330
331
|
// detached).
|
|
332
|
+
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- see above
|
|
331
333
|
}, [open, portalTarget]);
|
|
332
334
|
|
|
333
335
|
// Keep the newest message in view as it streams in — and after a swap, when
|
|
334
336
|
// the re-portaled panel's scroll container is reborn at the top.
|
|
335
337
|
useEffect(() => {
|
|
336
338
|
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
|
|
339
|
+
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- both deps are triggers, not values read here: re-run per streamed message and per re-portaled panel
|
|
337
340
|
}, [messages, portalTarget]);
|
|
338
341
|
|
|
339
342
|
const runQuestion = (raw: string) => {
|
|
@@ -57,7 +57,7 @@ const useClientData = (): BlumeClientData | null => {
|
|
|
57
57
|
// Intentional post-mount hydration guard: `null` on the server and first
|
|
58
58
|
// client render so hydration matches, then the snapshot once mounted. The
|
|
59
59
|
// extra render is required; do not seed the initial value from the DOM.
|
|
60
|
-
// oxlint-disable-next-line react/react-compiler, react-doctor/no-initialize-state -- deliberate SSR hydration guard
|
|
60
|
+
// oxlint-disable-next-line react/react-compiler, react/set-state-in-effect, react-doctor/no-initialize-state -- deliberate SSR hydration guard
|
|
61
61
|
useEffect(() => setData(readClientData()), []);
|
|
62
62
|
return data;
|
|
63
63
|
};
|
|
@@ -109,6 +109,7 @@ export const useSearch = (): UseSearch => {
|
|
|
109
109
|
// Before the lazy client import: the first search's heaviest phase is
|
|
110
110
|
// creating the provider client (index download), and it must show loading.
|
|
111
111
|
setLoading(true);
|
|
112
|
+
// oxlint-disable-next-line react/todo -- React Compiler cannot lower try/finally; the hook stays manually memoized above
|
|
112
113
|
try {
|
|
113
114
|
if (!searchFn.current) {
|
|
114
115
|
const { createSearch } = await import("blume:search-client");
|
|
@@ -250,6 +251,7 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
|
|
|
250
251
|
assistant.content = errorMessage;
|
|
251
252
|
setMessages([...history, { ...assistant }]);
|
|
252
253
|
}
|
|
254
|
+
// oxlint-disable-next-line react/todo -- React Compiler cannot lower try/finally; the hook stays manually memoized above
|
|
253
255
|
} finally {
|
|
254
256
|
if (live()) {
|
|
255
257
|
setLoading(false);
|
|
@@ -104,6 +104,7 @@ const menuRowClass =
|
|
|
104
104
|
data-blume-page-actions
|
|
105
105
|
data-i18n-chat-prompt={a.openInChatPrompt}
|
|
106
106
|
data-i18n-copied={a.copied}
|
|
107
|
+
data-i18n-copy-failed={a.copyFailed}
|
|
107
108
|
data-i18n-generating={a.generating}
|
|
108
109
|
data-mcp-name={mcpName ?? undefined}
|
|
109
110
|
data-mcp-url={mcpUrl ?? undefined}
|
|
@@ -259,7 +260,7 @@ const menuRowClass =
|
|
|
259
260
|
</div>
|
|
260
261
|
|
|
261
262
|
<script>
|
|
262
|
-
import { copyText, flashLabel } from "../copy-feedback.ts";
|
|
263
|
+
import { copyDeferredText, copyText, flashLabel } from "../copy-feedback.ts";
|
|
263
264
|
import { installDropdownDismiss } from "../dropdown-dismiss.ts";
|
|
264
265
|
import { prefixBase } from "../islands/base-path.ts";
|
|
265
266
|
import { rafThrottle } from "../raf-throttle.ts";
|
|
@@ -393,6 +394,8 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
393
394
|
const mdRoute = root.getAttribute("data-md") ?? "";
|
|
394
395
|
const md = prefixBase(import.meta.env.BASE_URL, mdRoute);
|
|
395
396
|
const copiedLabel = root.getAttribute("data-i18n-copied") || "Copied!";
|
|
397
|
+
const copyFailedLabel =
|
|
398
|
+
root.getAttribute("data-i18n-copy-failed") || "Copy failed";
|
|
396
399
|
const generatingLabel =
|
|
397
400
|
root.getAttribute("data-i18n-generating") || "Generating…";
|
|
398
401
|
// Always stamped: the frontmatter merges `EN_UI.actions` under any
|
|
@@ -444,8 +447,9 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
444
447
|
);
|
|
445
448
|
|
|
446
449
|
const copy = async (value: string, el: Element | null) => {
|
|
447
|
-
|
|
448
|
-
|
|
450
|
+
const copied = await copyText(value);
|
|
451
|
+
if (el instanceof HTMLElement) {
|
|
452
|
+
flashLabel(el, copied ? copiedLabel : copyFailedLabel);
|
|
449
453
|
}
|
|
450
454
|
};
|
|
451
455
|
root
|
|
@@ -475,21 +479,28 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
475
479
|
root
|
|
476
480
|
.querySelector("[data-blume-copy-page]")
|
|
477
481
|
?.addEventListener("click", async () => {
|
|
482
|
+
let copied = false;
|
|
478
483
|
try {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
484
|
+
// The Markdown is fetched on click; the deferred write keeps the
|
|
485
|
+
// clipboard call inside the click's user activation (Safari and
|
|
486
|
+
// Firefox reject a write issued after an `await`).
|
|
487
|
+
copied = await copyDeferredText(async () => {
|
|
488
|
+
const response = await fetch(md);
|
|
489
|
+
if (!response.ok) {
|
|
490
|
+
throw new Error(`Fetching ${md} failed (${response.status})`);
|
|
491
|
+
}
|
|
492
|
+
return response.text();
|
|
493
|
+
});
|
|
489
494
|
} catch (error) {
|
|
490
|
-
// Clipboard or fetch unavailable; don't flash "Copied!" untruthfully.
|
|
491
495
|
console.error("[blume] Copy as Markdown failed", error);
|
|
492
496
|
}
|
|
497
|
+
// Never flash "Copied!" untruthfully — but never stay silent either:
|
|
498
|
+
// a reader in an in-app browser or on a denied clipboard prompt must
|
|
499
|
+
// see that nothing landed on the clipboard. The legacy copy command
|
|
500
|
+
// has already been tried by the time the helper reports `false`.
|
|
501
|
+
if (label instanceof HTMLElement) {
|
|
502
|
+
flashLabel(label, copied ? copiedLabel : copyFailedLabel);
|
|
503
|
+
}
|
|
493
504
|
});
|
|
494
505
|
|
|
495
506
|
root
|
package/src/core/config-input.ts
CHANGED
|
@@ -788,6 +788,15 @@ export interface McpConfig {
|
|
|
788
788
|
* hosted MCP server.
|
|
789
789
|
*/
|
|
790
790
|
export interface AiConfig {
|
|
791
|
+
/**
|
|
792
|
+
* The JSON docs API — the REST twin of the MCP tools. Serves the page index
|
|
793
|
+
* (`/api/docs/pages.json`), each page as JSON (`/api/docs/pages/{route}.json`),
|
|
794
|
+
* and the navigation tree (`/api/docs/navigation.json`) as prerendered
|
|
795
|
+
* files, plus full-text search (`/api/docs/search?q=`) on server output,
|
|
796
|
+
* all described by an OpenAPI document at `/openapi.json`. Errors are RFC
|
|
797
|
+
* 9457 problem details. Defaults to `true`; set `false` to publish none of it.
|
|
798
|
+
*/
|
|
799
|
+
api?: boolean;
|
|
791
800
|
/** The Ask AI chat assistant. */
|
|
792
801
|
ask?: AskConfig;
|
|
793
802
|
/**
|
package/src/core/data.ts
CHANGED
|
@@ -78,6 +78,11 @@ export interface BlumeRoute {
|
|
|
78
78
|
alternates: RouteAlternate[];
|
|
79
79
|
/** Astro collection the entry renders through (`"docs"` | `"staged"`). */
|
|
80
80
|
collection: string;
|
|
81
|
+
/**
|
|
82
|
+
* Meta description as the page head renders it (`seo.description` over the
|
|
83
|
+
* front matter `description`), or `null` when the page declares neither.
|
|
84
|
+
*/
|
|
85
|
+
description: string | null;
|
|
81
86
|
draft: boolean;
|
|
82
87
|
/** "Edit this page" URL, or `null` when no repo/source provides one. */
|
|
83
88
|
editUrl: string | null;
|
|
@@ -133,7 +138,13 @@ export interface BlumeDataConfig {
|
|
|
133
138
|
* `deployment.site`, the condition under which one is emitted) feeds the
|
|
134
139
|
* 404 page's recovery links rather than the head.
|
|
135
140
|
*/
|
|
136
|
-
discovery: {
|
|
141
|
+
discovery: {
|
|
142
|
+
agentReadability: boolean;
|
|
143
|
+
/** Whether the JSON docs API and its `/openapi.json` are published. */
|
|
144
|
+
api: boolean;
|
|
145
|
+
llmsTxt: boolean;
|
|
146
|
+
sitemap: boolean;
|
|
147
|
+
};
|
|
137
148
|
favicon: BlumeFavicon;
|
|
138
149
|
feedback: boolean;
|
|
139
150
|
/**
|
|
@@ -161,7 +172,11 @@ export interface BlumeDataConfig {
|
|
|
161
172
|
* kept out of this snapshot, which pages serialize into HTML.
|
|
162
173
|
*/
|
|
163
174
|
og: {
|
|
164
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Site-wide card subtitle: `seo.og.description` (`false` omits it) over
|
|
177
|
+
* the site description. A page with its own description shows that
|
|
178
|
+
* instead; this is the fallback for pages without one.
|
|
179
|
+
*/
|
|
165
180
|
description?: string;
|
|
166
181
|
enabled: boolean;
|
|
167
182
|
/** Inlined SVG brand mark; `false` renders the card without any mark. */
|
|
@@ -4,6 +4,7 @@ import type { HydrationMode } from "./schema.ts";
|
|
|
4
4
|
* A reference to a component. Either an imported component (preferred, for type
|
|
5
5
|
* safety) or a string path resolved relative to the project root.
|
|
6
6
|
*/
|
|
7
|
+
// oxlint-disable-next-line anti-slop/no-unknown-type-aliases -- deliberately untyped: user configs pass imported components from any framework (React functions, Svelte classes, Vue SFC objects), which share no structural type
|
|
7
8
|
export type ComponentReference = unknown | string;
|
|
8
9
|
|
|
9
10
|
/** An interactive component plus its hydration strategy. */
|
|
@@ -15,6 +16,7 @@ export interface IslandDescriptor {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/** A component override: a static component or a hydrated island. */
|
|
19
|
+
// oxlint-disable-next-line anti-slop/no-unknown-type-aliases -- inherits the untyped `ComponentReference` above
|
|
18
20
|
export type ComponentOverride = ComponentReference | IslandDescriptor;
|
|
19
21
|
|
|
20
22
|
/** User-authored component overrides, grouped by surface. */
|
package/src/core/i18n-ui.ts
CHANGED
|
@@ -21,6 +21,7 @@ const uiStringsObject = z.object({
|
|
|
21
21
|
copyClaudeCode: z.string().default("Copy Claude Code command"),
|
|
22
22
|
copyCode: z.string().default("Copy code"),
|
|
23
23
|
copyCodex: z.string().default("Copy Codex command"),
|
|
24
|
+
copyFailed: z.string().default("Copy failed"),
|
|
24
25
|
copyMarkdown: z.string().default("Copy as Markdown"),
|
|
25
26
|
copyServerUrl: z.string().default("Copy server URL"),
|
|
26
27
|
edit: z.string().default("Edit on GitHub"),
|
|
@@ -108,6 +109,8 @@ const uiStringsObject = z.object({
|
|
|
108
109
|
.prefault({}),
|
|
109
110
|
notFound: z
|
|
110
111
|
.object({
|
|
112
|
+
/** Label of the OpenAPI description link on the Markdown/JSON 404. */
|
|
113
|
+
api: z.string().default("JSON API description (openapi.json)"),
|
|
111
114
|
description: z
|
|
112
115
|
.string()
|
|
113
116
|
.default("We couldn't find the page you're looking for."),
|
package/src/core/includes.ts
CHANGED
|
@@ -622,7 +622,8 @@ export const buildIncludeGraph = (
|
|
|
622
622
|
continue;
|
|
623
623
|
}
|
|
624
624
|
for (const partial of page.includes ?? []) {
|
|
625
|
-
const includers =
|
|
625
|
+
const includers = graph[partial] ?? [];
|
|
626
|
+
graph[partial] = includers;
|
|
626
627
|
if (!includers.includes(sourcePath)) {
|
|
627
628
|
includers.push(sourcePath);
|
|
628
629
|
}
|
package/src/core/manifest.ts
CHANGED
|
@@ -63,6 +63,14 @@ export const contentIndexable = (
|
|
|
63
63
|
* output is fully prerendered (render-fallback, no client redirect). Fallback
|
|
64
64
|
* routes are not indexed and carry no `hreflang` of their own.
|
|
65
65
|
*/
|
|
66
|
+
/**
|
|
67
|
+
* The description a page's `<head>` emits — `seo.description` over the front
|
|
68
|
+
* matter `description` — mirrored onto its route so the OG card's subtitle
|
|
69
|
+
* matches the page's `og:description`.
|
|
70
|
+
*/
|
|
71
|
+
const routeDescription = (page: PageRecord): string | undefined =>
|
|
72
|
+
page.meta.seo.description ?? page.description;
|
|
73
|
+
|
|
66
74
|
const buildFallbackRoutes = (
|
|
67
75
|
graph: ContentGraph,
|
|
68
76
|
i18n: NonNullable<ResolvedConfig["i18n"]>,
|
|
@@ -108,6 +116,7 @@ const buildFallbackRoutes = (
|
|
|
108
116
|
alternates: alternatesByKey.get(key) ?? [],
|
|
109
117
|
collection: source.collection ?? "docs",
|
|
110
118
|
contentType: source.contentType,
|
|
119
|
+
description: routeDescription(source),
|
|
111
120
|
draft: source.meta.draft,
|
|
112
121
|
editUrl: source.editUrl,
|
|
113
122
|
entryId: source.entryId ?? source.source.ref,
|
|
@@ -171,6 +180,7 @@ export const buildManifest = (options: {
|
|
|
171
180
|
alternates: alternatesByKey.get(page.translationKey) ?? [],
|
|
172
181
|
collection: page.collection ?? "docs",
|
|
173
182
|
contentType: page.contentType,
|
|
183
|
+
description: routeDescription(page),
|
|
174
184
|
draft: page.meta.draft,
|
|
175
185
|
editUrl: page.editUrl,
|
|
176
186
|
entryId: page.entryId ?? page.source.ref,
|
package/src/core/schema.ts
CHANGED
|
@@ -838,6 +838,13 @@ const llmsTxtObjectSchema = z.strictObject({
|
|
|
838
838
|
type LlmsTxtResolved = z.output<typeof llmsTxtObjectSchema>;
|
|
839
839
|
|
|
840
840
|
const aiConfigSchema = z.strictObject({
|
|
841
|
+
/**
|
|
842
|
+
* The JSON docs API: the page index, per-page JSON, and navigation under
|
|
843
|
+
* `/api/docs/` (prerendered, so a static site serves them from files), the
|
|
844
|
+
* live search endpoint on server output, and the OpenAPI description of
|
|
845
|
+
* the whole machine-readable surface at `/openapi.json`. On by default.
|
|
846
|
+
*/
|
|
847
|
+
api: z.boolean().default(true),
|
|
841
848
|
ask: z
|
|
842
849
|
.strictObject({
|
|
843
850
|
// Name of the env var holding the provider's API key; each provider has
|
|
@@ -907,9 +914,8 @@ const aiConfigSchema = z.strictObject({
|
|
|
907
914
|
llmsTxt: z
|
|
908
915
|
.union([z.boolean(), llmsTxtObjectSchema])
|
|
909
916
|
.default(true)
|
|
910
|
-
.transform(
|
|
911
|
-
(value):
|
|
912
|
-
isBoolean(value) ? { enabled: value, openapi: true } : value
|
|
917
|
+
.transform((value): LlmsTxtResolved =>
|
|
918
|
+
isBoolean(value) ? { enabled: value, openapi: true } : value
|
|
913
919
|
),
|
|
914
920
|
// Serializers for the agent-facing Markdown downlevel (the `.md` mirror,
|
|
915
921
|
// llms-full.txt, MCP get_page), keyed by JSX name. Functions live here —
|
|
@@ -1332,8 +1338,9 @@ const ogFontSchema = z.union([
|
|
|
1332
1338
|
|
|
1333
1339
|
const ogConfigSchema = z.strictObject({
|
|
1334
1340
|
/**
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1341
|
+
* Site-wide card subtitle, shown on pages without their own description.
|
|
1342
|
+
* Defaults to the site description; a string overrides it, `false` renders
|
|
1343
|
+
* every card without a subtitle (page descriptions included).
|
|
1337
1344
|
*/
|
|
1338
1345
|
description: z.union([z.string(), z.literal(false)]).optional(),
|
|
1339
1346
|
/**
|
|
@@ -1708,6 +1715,14 @@ const openapiSourceSchema = z.strictObject({
|
|
|
1708
1715
|
noindex: z.boolean().default(false),
|
|
1709
1716
|
/** Per-source route; defaults to the block's `route` (or a derived path). */
|
|
1710
1717
|
route: z.string().optional(),
|
|
1718
|
+
/**
|
|
1719
|
+
* Append the English "Reference for the … endpoint in the … API." sentence
|
|
1720
|
+
* to every generated operation page's meta description. On by default, so
|
|
1721
|
+
* terse specs still ship distinct, snippet-length descriptions; set to
|
|
1722
|
+
* `false` on a non-English site to describe pages with the spec's own prose
|
|
1723
|
+
* alone (falling back to the page title when an operation has none).
|
|
1724
|
+
*/
|
|
1725
|
+
seoDescriptionSuffix: z.boolean().default(true),
|
|
1711
1726
|
/** Local path or `http(s)` URL to the spec. */
|
|
1712
1727
|
spec: z.string(),
|
|
1713
1728
|
});
|
package/src/core/types.ts
CHANGED
|
@@ -368,6 +368,12 @@ export interface RouteManifestEntry {
|
|
|
368
368
|
/** Adapter-supplied "edit this page" URL (non-filesystem sources). */
|
|
369
369
|
editUrl?: string;
|
|
370
370
|
title: string;
|
|
371
|
+
/**
|
|
372
|
+
* The page's meta description as the head renders it: `seo.description`
|
|
373
|
+
* over the front matter `description`. Feeds the generated OG card's
|
|
374
|
+
* subtitle, so a shared link's image and its `og:description` agree.
|
|
375
|
+
*/
|
|
376
|
+
description?: string;
|
|
371
377
|
contentType: string;
|
|
372
378
|
hidden: boolean;
|
|
373
379
|
draft: boolean;
|
package/src/core/ui-packs/ar.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const ar: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "نسخ أمر Claude Code",
|
|
12
12
|
copyCode: "نسخ الكود",
|
|
13
13
|
copyCodex: "نسخ أمر Codex",
|
|
14
|
+
copyFailed: "فشل النسخ",
|
|
14
15
|
copyMarkdown: "نسخ بصيغة Markdown",
|
|
15
16
|
copyServerUrl: "نسخ عنوان URL للخادم",
|
|
16
17
|
edit: "التعديل على GitHub",
|
package/src/core/ui-packs/bg.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const bg: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Копирай командата на Claude Code",
|
|
12
12
|
copyCode: "Копирай кода",
|
|
13
13
|
copyCodex: "Копирай командата на Codex",
|
|
14
|
+
copyFailed: "Копирането не бе успешно",
|
|
14
15
|
copyMarkdown: "Копирай като Markdown",
|
|
15
16
|
copyServerUrl: "Копирай URL на сървъра",
|
|
16
17
|
edit: "Редактирай в GitHub",
|
package/src/core/ui-packs/bn.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const bn: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Claude Code কমান্ড অনুলিপি করুন",
|
|
12
12
|
copyCode: "কোড অনুলিপি করুন",
|
|
13
13
|
copyCodex: "Codex কমান্ড অনুলিপি করুন",
|
|
14
|
+
copyFailed: "অনুলিপি ব্যর্থ হয়েছে",
|
|
14
15
|
copyMarkdown: "Markdown হিসেবে অনুলিপি করুন",
|
|
15
16
|
copyServerUrl: "সার্ভার URL অনুলিপি করুন",
|
|
16
17
|
edit: "GitHub-এ সম্পাদনা করুন",
|
package/src/core/ui-packs/ca.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const ca: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Copia l'ordre de Claude Code",
|
|
12
12
|
copyCode: "Copia el codi",
|
|
13
13
|
copyCodex: "Copia l'ordre de Codex",
|
|
14
|
+
copyFailed: "No s'ha pogut copiar",
|
|
14
15
|
copyMarkdown: "Copia com a Markdown",
|
|
15
16
|
copyServerUrl: "Copia l'URL del servidor",
|
|
16
17
|
edit: "Edita a GitHub",
|
package/src/core/ui-packs/cs.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const cs: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Kopírovat příkaz Claude Code",
|
|
12
12
|
copyCode: "Kopírovat kód",
|
|
13
13
|
copyCodex: "Kopírovat příkaz Codex",
|
|
14
|
+
copyFailed: "Kopírování se nezdařilo",
|
|
14
15
|
copyMarkdown: "Kopírovat jako Markdown",
|
|
15
16
|
copyServerUrl: "Kopírovat URL serveru",
|
|
16
17
|
edit: "Upravit na GitHubu",
|
package/src/core/ui-packs/da.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const da: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Kopiér Claude Code-kommando",
|
|
12
12
|
copyCode: "Kopiér kode",
|
|
13
13
|
copyCodex: "Kopiér Codex-kommando",
|
|
14
|
+
copyFailed: "Kopiering mislykkedes",
|
|
14
15
|
copyMarkdown: "Kopiér som Markdown",
|
|
15
16
|
copyServerUrl: "Kopiér server-URL",
|
|
16
17
|
edit: "Rediger på GitHub",
|
package/src/core/ui-packs/de.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const de: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Claude-Code-Befehl kopieren",
|
|
12
12
|
copyCode: "Code kopieren",
|
|
13
13
|
copyCodex: "Codex-Befehl kopieren",
|
|
14
|
+
copyFailed: "Kopieren fehlgeschlagen",
|
|
14
15
|
copyMarkdown: "Als Markdown kopieren",
|
|
15
16
|
copyServerUrl: "Server-URL kopieren",
|
|
16
17
|
edit: "Auf GitHub bearbeiten",
|
package/src/core/ui-packs/el.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const el: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Αντιγραφή εντολής Claude Code",
|
|
12
12
|
copyCode: "Αντιγραφή κώδικα",
|
|
13
13
|
copyCodex: "Αντιγραφή εντολής Codex",
|
|
14
|
+
copyFailed: "Η αντιγραφή απέτυχε",
|
|
14
15
|
copyMarkdown: "Αντιγραφή ως Markdown",
|
|
15
16
|
copyServerUrl: "Αντιγραφή URL διακομιστή",
|
|
16
17
|
edit: "Επεξεργασία στο GitHub",
|
package/src/core/ui-packs/es.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const es: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Copiar comando de Claude Code",
|
|
12
12
|
copyCode: "Copiar código",
|
|
13
13
|
copyCodex: "Copiar comando de Codex",
|
|
14
|
+
copyFailed: "Error al copiar",
|
|
14
15
|
copyMarkdown: "Copiar como Markdown",
|
|
15
16
|
copyServerUrl: "Copiar URL del servidor",
|
|
16
17
|
edit: "Editar en GitHub",
|
package/src/core/ui-packs/fa.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const fa: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "کپی دستور Claude Code",
|
|
12
12
|
copyCode: "کپی کد",
|
|
13
13
|
copyCodex: "کپی دستور Codex",
|
|
14
|
+
copyFailed: "کپی ناموفق بود",
|
|
14
15
|
copyMarkdown: "کپی بهصورت Markdown",
|
|
15
16
|
copyServerUrl: "کپی نشانی سرور",
|
|
16
17
|
edit: "ویرایش در GitHub",
|
package/src/core/ui-packs/fi.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const fi: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Kopioi Claude Code -komento",
|
|
12
12
|
copyCode: "Kopioi koodi",
|
|
13
13
|
copyCodex: "Kopioi Codex-komento",
|
|
14
|
+
copyFailed: "Kopiointi epäonnistui",
|
|
14
15
|
copyMarkdown: "Kopioi Markdownina",
|
|
15
16
|
copyServerUrl: "Kopioi palvelimen URL",
|
|
16
17
|
edit: "Muokkaa GitHubissa",
|
package/src/core/ui-packs/fr.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const fr: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Copier la commande Claude Code",
|
|
12
12
|
copyCode: "Copier le code",
|
|
13
13
|
copyCodex: "Copier la commande Codex",
|
|
14
|
+
copyFailed: "Échec de la copie",
|
|
14
15
|
copyMarkdown: "Copier en Markdown",
|
|
15
16
|
copyServerUrl: "Copier l'URL du serveur",
|
|
16
17
|
edit: "Modifier sur GitHub",
|
package/src/core/ui-packs/he.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const he: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "העתק פקודת Claude Code",
|
|
12
12
|
copyCode: "העתק קוד",
|
|
13
13
|
copyCodex: "העתק פקודת Codex",
|
|
14
|
+
copyFailed: "ההעתקה נכשלה",
|
|
14
15
|
copyMarkdown: "העתק כ-Markdown",
|
|
15
16
|
copyServerUrl: "העתק כתובת URL של השרת",
|
|
16
17
|
edit: "ערוך ב-GitHub",
|
package/src/core/ui-packs/hi.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const hi: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Claude Code कमांड कॉपी करें",
|
|
12
12
|
copyCode: "कोड कॉपी करें",
|
|
13
13
|
copyCodex: "Codex कमांड कॉपी करें",
|
|
14
|
+
copyFailed: "कॉपी विफल रहा",
|
|
14
15
|
copyMarkdown: "Markdown के रूप में कॉपी करें",
|
|
15
16
|
copyServerUrl: "सर्वर URL कॉपी करें",
|
|
16
17
|
edit: "GitHub पर संपादित करें",
|
package/src/core/ui-packs/hr.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const hr: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Kopiraj naredbu Claude Code",
|
|
12
12
|
copyCode: "Kopiraj kôd",
|
|
13
13
|
copyCodex: "Kopiraj naredbu Codex",
|
|
14
|
+
copyFailed: "Kopiranje nije uspjelo",
|
|
14
15
|
copyMarkdown: "Kopiraj kao Markdown",
|
|
15
16
|
copyServerUrl: "Kopiraj URL poslužitelja",
|
|
16
17
|
edit: "Uredi na GitHubu",
|
package/src/core/ui-packs/hu.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const hu: UIStringsOverride = {
|
|
|
11
11
|
copyClaudeCode: "Claude Code parancs másolása",
|
|
12
12
|
copyCode: "Kód másolása",
|
|
13
13
|
copyCodex: "Codex parancs másolása",
|
|
14
|
+
copyFailed: "A másolás nem sikerült",
|
|
14
15
|
copyMarkdown: "Másolás Markdownként",
|
|
15
16
|
copyServerUrl: "Szerver URL másolása",
|
|
16
17
|
edit: "Szerkesztés a GitHubon",
|