blume 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/cli/index.js +496 -295
- package/dist/cli/index.js.map +18 -17
- package/dist/types/core/config-input.d.ts +44 -22
- package/dist/types/core/config.d.ts +3 -3
- package/dist/types/core/data.d.ts +12 -0
- package/dist/types/core/i18n-ui.d.ts +136 -136
- package/dist/types/core/schema.d.ts +502 -384
- package/dist/types/core/types.d.ts +10 -0
- package/dist/types/openapi/references.d.ts +12 -7
- package/docs/advanced/api-reference.mdx +11 -3
- package/docs/advanced/custom-pages.mdx +2 -0
- package/docs/configuration/ai.mdx +6 -4
- package/docs/configuration/index.mdx +6 -8
- package/docs/configuration/seo.mdx +20 -1
- package/docs/content/components.mdx +26 -5
- package/docs/content/navigation.mdx +10 -0
- package/docs/content/syntax.mdx +116 -4
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +170 -0
- package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
- package/skills/blume-migrate/references/docusaurus.md +95 -0
- package/skills/blume-migrate/references/fumadocs.md +95 -0
- package/skills/blume-migrate/references/mintlify.md +155 -0
- package/skills/blume-migrate/references/monorepo.md +224 -0
- package/skills/blume-migrate/references/nextra.md +76 -0
- package/skills/blume-migrate/references/starlight.md +116 -0
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +466 -0
- package/src/ai/agent-readability.ts +3 -3
- package/src/ai/mcp/data.ts +2 -2
- package/src/astro/component-slots.ts +3 -2
- package/src/astro/generate.ts +97 -30
- package/src/astro/templates.ts +140 -53
- package/src/blume-modules.d.ts +6 -0
- package/src/components/content/Callout.astro +8 -2
- package/src/components/content/Prompt.astro +25 -13
- package/src/components/layout/Header.astro +19 -10
- package/src/components/layout/Logo.astro +13 -1
- package/src/components/layout/PageFeedback.astro +1 -1
- package/src/components/layout/PageLayout.astro +11 -11
- package/src/components/layout/Pagination.astro +6 -6
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +10 -11
- package/src/components/layout/Search.astro +1 -1
- package/src/components/layout/nav-utils.ts +9 -7
- package/src/core/config-input.ts +47 -27
- package/src/core/config.ts +3 -3
- package/src/core/data.ts +9 -1
- package/src/core/navigation.ts +55 -13
- package/src/core/schema.ts +32 -15
- package/src/core/server-features.ts +1 -1
- package/src/core/sources/watch.ts +5 -0
- package/src/core/types.ts +10 -0
- package/src/deploy/adapter-output.ts +11 -1
- package/src/markdown/index.ts +2 -0
- package/src/markdown/language-icon.ts +2 -1
- package/src/markdown/table-wrap.ts +43 -0
- package/src/og/card.ts +39 -12
- package/src/og/index.ts +1 -1
- package/src/og/logo.ts +21 -0
- package/src/openapi/references.ts +19 -16
- package/src/registry/eject.ts +11 -5
- package/src/theme/entry.ts +50 -5
|
@@ -39,7 +39,7 @@ import Favicon from "./Favicon.astro";
|
|
|
39
39
|
import Fonts from "./Fonts.astro";
|
|
40
40
|
import { BANNER_INIT_SCRIPT, THEME_INIT_SCRIPT } from "./head-scripts.ts";
|
|
41
41
|
import Header from "./Header.astro";
|
|
42
|
-
import {
|
|
42
|
+
import { activeTabForRoute } from "./nav-utils.ts";
|
|
43
43
|
|
|
44
44
|
interface Props {
|
|
45
45
|
site: { title: string; description?: string };
|
|
@@ -58,6 +58,11 @@ interface Props {
|
|
|
58
58
|
themeMode: "system" | "light" | "dark";
|
|
59
59
|
fontCssVars?: string[];
|
|
60
60
|
searchEnabled: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Opt this page out of the header's Ask AI trigger. Defaults to whether Ask
|
|
63
|
+
* AI is configured, so a custom page gets the same trigger the docs pages have
|
|
64
|
+
* without wiring anything up.
|
|
65
|
+
*/
|
|
61
66
|
askEnabled?: boolean;
|
|
62
67
|
/**
|
|
63
68
|
* Absolute site URL (`data.config.site`). When set, `canonical` and the
|
|
@@ -132,6 +137,7 @@ const searchLocale =
|
|
|
132
137
|
const pageTitle = page?.title ?? site.title;
|
|
133
138
|
const description = page?.description ?? site.description;
|
|
134
139
|
const route = page?.route ?? "/";
|
|
140
|
+
const activeTab = activeTabForRoute(navigation.tabs, route);
|
|
135
141
|
|
|
136
142
|
// Derive canonical + og:image from the site URL the same way the catch-all does
|
|
137
143
|
// for content pages, so a custom page gets both for free. The matching OG card
|
|
@@ -229,6 +235,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
229
235
|
<Banner banner={banner} strings={strings.banner} />
|
|
230
236
|
<Header
|
|
231
237
|
askEnabled={askEnabled}
|
|
238
|
+
askStrings={strings.ask}
|
|
232
239
|
hasSidebar={false}
|
|
233
240
|
localeSwitch={localeSwitch}
|
|
234
241
|
logo={logo}
|
|
@@ -240,9 +247,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
240
247
|
searchStrings={strings.search}
|
|
241
248
|
site={site}
|
|
242
249
|
switcherStrings={strings.languageSwitcher}
|
|
243
|
-
|
|
244
|
-
<slot name="ask" slot="ask" />
|
|
245
|
-
</Header>
|
|
250
|
+
/>
|
|
246
251
|
<main id="blume-content"><slot /></main>
|
|
247
252
|
<slot name="footer" />
|
|
248
253
|
{
|
|
@@ -263,14 +268,9 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
263
268
|
{navigation.tabs.map((tab) => (
|
|
264
269
|
<li>
|
|
265
270
|
<a
|
|
266
|
-
aria-current={
|
|
267
|
-
route === tab.path ||
|
|
268
|
-
(tab.path !== "/" && isUnderPath(route, tab.path))
|
|
269
|
-
? "page"
|
|
270
|
-
: undefined
|
|
271
|
-
}
|
|
271
|
+
aria-current={tab === activeTab ? "page" : undefined}
|
|
272
272
|
class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
|
|
273
|
-
href={withBase(tab.path)}
|
|
273
|
+
href={withBase(tab.href ?? tab.path)}
|
|
274
274
|
>
|
|
275
275
|
{tab.label}
|
|
276
276
|
</a>
|
|
@@ -30,15 +30,15 @@ const s = { ...EN_UI.page, ...strings };
|
|
|
30
30
|
>
|
|
31
31
|
{prev ? (
|
|
32
32
|
<a
|
|
33
|
-
class="flex max-w-[48%] flex-1 items-center gap-2 rounded-
|
|
33
|
+
class="flex max-w-[48%] flex-1 items-center gap-2 rounded-blume border border-border px-4 py-3 text-foreground transition-colors hover:border-foreground max-md:max-w-full"
|
|
34
34
|
href={withBase(prev.route)}
|
|
35
35
|
>
|
|
36
36
|
<Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
|
|
37
|
-
<span>
|
|
37
|
+
<span class="min-w-0">
|
|
38
38
|
<span class="block text-muted-foreground text-xs max-md:hidden">
|
|
39
39
|
{s.previous}
|
|
40
40
|
</span>
|
|
41
|
-
<span class="block font-medium">{prev.label}</span>
|
|
41
|
+
<span class="block truncate font-medium">{prev.label}</span>
|
|
42
42
|
</span>
|
|
43
43
|
</a>
|
|
44
44
|
) : (
|
|
@@ -46,14 +46,14 @@ const s = { ...EN_UI.page, ...strings };
|
|
|
46
46
|
)}
|
|
47
47
|
{next && (
|
|
48
48
|
<a
|
|
49
|
-
class="ms-auto flex max-w-[48%] flex-1 items-center justify-end gap-2 rounded-
|
|
49
|
+
class="ms-auto flex max-w-[48%] flex-1 items-center justify-end gap-2 rounded-blume border border-border px-4 py-3 text-end text-foreground transition-colors hover:border-foreground max-md:max-w-full"
|
|
50
50
|
href={withBase(next.route)}
|
|
51
51
|
>
|
|
52
|
-
<span>
|
|
52
|
+
<span class="min-w-0">
|
|
53
53
|
<span class="block text-muted-foreground text-xs max-md:hidden">
|
|
54
54
|
{s.next}
|
|
55
55
|
</span>
|
|
56
|
-
<span class="block font-medium">{next.label}</span>
|
|
56
|
+
<span class="block truncate font-medium">{next.label}</span>
|
|
57
57
|
</span>
|
|
58
58
|
<Icon class="rtl:-scale-x-100" name="arrow-right" size={16} />
|
|
59
59
|
</a>
|
|
@@ -27,10 +27,10 @@ import { BANNER_INIT_SCRIPT, THEME_INIT_SCRIPT } from "./head-scripts.ts";
|
|
|
27
27
|
import Header from "./Header.astro";
|
|
28
28
|
import Icon from "../Icon.astro";
|
|
29
29
|
import {
|
|
30
|
+
activeTabForRoute,
|
|
30
31
|
findBreadcrumbs,
|
|
31
32
|
flattenPages,
|
|
32
33
|
getPagination,
|
|
33
|
-
isUnderPath,
|
|
34
34
|
sidebarForRoute,
|
|
35
35
|
} from "./nav-utils.ts";
|
|
36
36
|
import NavTree from "./NavTree.astro";
|
|
@@ -100,6 +100,10 @@ interface Props {
|
|
|
100
100
|
x?: { creator?: string; handle?: string };
|
|
101
101
|
canonical?: string | null;
|
|
102
102
|
editUrl?: string | null;
|
|
103
|
+
/**
|
|
104
|
+
* Opt this page out of the header's Ask AI trigger. Defaults to whether Ask
|
|
105
|
+
* AI is configured, so pages never wire the trigger up themselves.
|
|
106
|
+
*/
|
|
103
107
|
askEnabled?: boolean;
|
|
104
108
|
/** Show the "Was this page helpful?" rating below the content. */
|
|
105
109
|
feedback?: boolean;
|
|
@@ -303,6 +307,7 @@ const mcpUrl =
|
|
|
303
307
|
// active tab's section, so a multi-section site drills each tab into its own
|
|
304
308
|
// pages. Without tabs — or on a route under none — this is the full sidebar.
|
|
305
309
|
const sidebar = sidebarForRoute(navigation.sidebar, navigation.tabs, page.route);
|
|
310
|
+
const activeTab = activeTabForRoute(navigation.tabs, page.route);
|
|
306
311
|
const crumbs = findBreadcrumbs(sidebar, page.route);
|
|
307
312
|
const { prev, next } = getPagination(flattenPages(sidebar), page.route);
|
|
308
313
|
|
|
@@ -430,6 +435,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
430
435
|
<Banner banner={banner} strings={strings.banner} />
|
|
431
436
|
<HeaderSlot
|
|
432
437
|
askEnabled={askEnabled}
|
|
438
|
+
askStrings={strings.ask}
|
|
433
439
|
layout={layout}
|
|
434
440
|
localeSwitch={localeSwitch}
|
|
435
441
|
logo={logo}
|
|
@@ -441,9 +447,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
441
447
|
searchStrings={strings.search}
|
|
442
448
|
site={site}
|
|
443
449
|
switcherStrings={strings.languageSwitcher}
|
|
444
|
-
|
|
445
|
-
<slot name="ask" slot="ask" />
|
|
446
|
-
</HeaderSlot>
|
|
450
|
+
/>
|
|
447
451
|
<div
|
|
448
452
|
class:list={["mx-auto grid grid-cols-1 items-start", gridClass]}
|
|
449
453
|
data-blume-doc-grid
|
|
@@ -523,14 +527,9 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
523
527
|
{navigation.tabs.map((tab) => (
|
|
524
528
|
<li>
|
|
525
529
|
<a
|
|
526
|
-
aria-current={
|
|
527
|
-
page.route === tab.path ||
|
|
528
|
-
(tab.path !== "/" && isUnderPath(page.route, tab.path))
|
|
529
|
-
? "page"
|
|
530
|
-
: undefined
|
|
531
|
-
}
|
|
530
|
+
aria-current={tab === activeTab ? "page" : undefined}
|
|
532
531
|
class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
|
|
533
|
-
href={withBase(tab.path)}
|
|
532
|
+
href={withBase(tab.href ?? tab.path)}
|
|
534
533
|
>
|
|
535
534
|
{tab.label}
|
|
536
535
|
</a>
|
|
@@ -58,7 +58,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
58
58
|
|
|
59
59
|
<dialog
|
|
60
60
|
aria-label={s.label}
|
|
61
|
-
class="
|
|
61
|
+
class="mx-auto mt-[8vh] mb-auto h-[min(480px,90dvh)] w-[min(62.5rem,94vw)] flex-col overflow-hidden rounded-blume border border-border bg-background/80 p-0 text-foreground shadow-2xl outline-none backdrop-blur-xl backdrop:bg-black/30 open:flex sm:mt-auto"
|
|
62
62
|
data-blume-search-dialog
|
|
63
63
|
>
|
|
64
64
|
<div
|
|
@@ -80,14 +80,16 @@ export const isUnderPath = (route: string, base: string): boolean =>
|
|
|
80
80
|
base === "/" || route === base || route.startsWith(`${base}/`);
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* The tab whose `path` is the longest prefix of `route
|
|
84
|
-
*
|
|
85
|
-
* so never scopes the sidebar.
|
|
83
|
+
* The tab whose `path` is the longest prefix of `route`. The root tab (`/`)
|
|
84
|
+
* acts as the fallback when no more specific tab matches.
|
|
86
85
|
*/
|
|
87
|
-
const
|
|
86
|
+
export const activeTabForRoute = (
|
|
87
|
+
tabs: NavTab[],
|
|
88
|
+
route: string
|
|
89
|
+
): NavTab | null => {
|
|
88
90
|
let match: NavTab | null = null;
|
|
89
91
|
for (const tab of tabs) {
|
|
90
|
-
if (
|
|
92
|
+
if (!isUnderPath(route, tab.path)) {
|
|
91
93
|
continue;
|
|
92
94
|
}
|
|
93
95
|
if (!match || tab.path.length > match.path.length) {
|
|
@@ -187,8 +189,8 @@ export const sidebarForRoute = (
|
|
|
187
189
|
tabs: NavTab[],
|
|
188
190
|
route: string
|
|
189
191
|
): NavNode[] => {
|
|
190
|
-
const tab =
|
|
191
|
-
if (tab) {
|
|
192
|
+
const tab = activeTabForRoute(tabs, route);
|
|
193
|
+
if (tab && tab.path !== "/") {
|
|
192
194
|
return sectionChildren(sidebar, tab.path) ?? [];
|
|
193
195
|
}
|
|
194
196
|
const scoped = withoutTabSections(sidebar, tabs);
|
package/src/core/config-input.ts
CHANGED
|
@@ -525,7 +525,22 @@ export interface LlmsTxtConfig {
|
|
|
525
525
|
openapi?: boolean;
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
/**
|
|
528
|
+
/** Expose the docs as an MCP server for connecting agents. */
|
|
529
|
+
export interface McpConfig {
|
|
530
|
+
/** Turn the MCP server on. Defaults to `false`. */
|
|
531
|
+
enabled?: boolean;
|
|
532
|
+
/** Optional system hint passed to connecting agents. */
|
|
533
|
+
instructions?: string;
|
|
534
|
+
/** Server name shown to clients; defaults to the site title. */
|
|
535
|
+
name?: string;
|
|
536
|
+
/** Route the server mounts at. Defaults to `/mcp`. */
|
|
537
|
+
route?: string;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* AI-facing features: the Ask AI assistant, an `llms.txt` manifest, and the
|
|
542
|
+
* hosted MCP server.
|
|
543
|
+
*/
|
|
529
544
|
export interface AiConfig {
|
|
530
545
|
/** The Ask AI chat assistant. */
|
|
531
546
|
ask?: AskConfig;
|
|
@@ -553,6 +568,8 @@ export interface AiConfig {
|
|
|
553
568
|
* ```
|
|
554
569
|
*/
|
|
555
570
|
markdownComponents?: Record<string, ComponentMarkdown>;
|
|
571
|
+
/** Expose the docs as an MCP server for agents. */
|
|
572
|
+
mcp?: McpConfig;
|
|
556
573
|
}
|
|
557
574
|
|
|
558
575
|
// ---------------------------------------------------------------------------
|
|
@@ -586,22 +603,6 @@ export interface AnalyticsConfig {
|
|
|
586
603
|
vercel?: boolean;
|
|
587
604
|
}
|
|
588
605
|
|
|
589
|
-
// ---------------------------------------------------------------------------
|
|
590
|
-
// MCP
|
|
591
|
-
// ---------------------------------------------------------------------------
|
|
592
|
-
|
|
593
|
-
/** Expose the docs as an MCP server for connecting agents. */
|
|
594
|
-
export interface McpConfig {
|
|
595
|
-
/** Turn the MCP server on. Defaults to `false`. */
|
|
596
|
-
enabled?: boolean;
|
|
597
|
-
/** Optional system hint passed to connecting agents. */
|
|
598
|
-
instructions?: string;
|
|
599
|
-
/** Server name shown to clients; defaults to the site title. */
|
|
600
|
-
name?: string;
|
|
601
|
-
/** Route the server mounts at. Defaults to `/mcp`. */
|
|
602
|
-
route?: string;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
606
|
// ---------------------------------------------------------------------------
|
|
606
607
|
// i18n
|
|
607
608
|
// ---------------------------------------------------------------------------
|
|
@@ -710,6 +711,34 @@ export interface RssConfig {
|
|
|
710
711
|
types?: string[];
|
|
711
712
|
}
|
|
712
713
|
|
|
714
|
+
/** Colors used by generated Open Graph cards. Values must be hex colors. */
|
|
715
|
+
export interface OgPaletteConfig {
|
|
716
|
+
/** Fallback mark color. Defaults to the light theme accent. */
|
|
717
|
+
accent?: string;
|
|
718
|
+
/** Card background. */
|
|
719
|
+
background?: string;
|
|
720
|
+
/** Footer divider. */
|
|
721
|
+
border?: string;
|
|
722
|
+
/** Headline and `currentColor` logo color. */
|
|
723
|
+
foreground?: string;
|
|
724
|
+
/** Description and footer text. */
|
|
725
|
+
muted?: string;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Per-page Open Graph image generation. */
|
|
729
|
+
export interface OgConfig {
|
|
730
|
+
/**
|
|
731
|
+
* Generate an OG image per page. Defaults to on once a deployment `site`
|
|
732
|
+
* URL is known and off otherwise (`og:image` must be absolute). An explicit
|
|
733
|
+
* value always wins.
|
|
734
|
+
*/
|
|
735
|
+
enabled?: boolean;
|
|
736
|
+
/** Local SVG used in the generated card instead of the site logo. */
|
|
737
|
+
logo?: string;
|
|
738
|
+
/** Optional generated-card colors. */
|
|
739
|
+
palette?: OgPaletteConfig;
|
|
740
|
+
}
|
|
741
|
+
|
|
713
742
|
/** Discoverability: OG images, feeds, sitemap, robots, and structured data. */
|
|
714
743
|
export interface SeoConfig {
|
|
715
744
|
/**
|
|
@@ -720,14 +749,7 @@ export interface SeoConfig {
|
|
|
720
749
|
/** robots.txt `Content-Signal` usage declaration. Defaults to `true`. */
|
|
721
750
|
contentSignals?: ContentSignalsConfig;
|
|
722
751
|
/** Per-page Open Graph image generation. */
|
|
723
|
-
og?:
|
|
724
|
-
/**
|
|
725
|
-
* Generate an OG image per page. Defaults to on once a deployment `site`
|
|
726
|
-
* URL is known and off otherwise (`og:image` must be absolute). An explicit
|
|
727
|
-
* value always wins.
|
|
728
|
-
*/
|
|
729
|
-
enabled?: boolean;
|
|
730
|
-
};
|
|
752
|
+
og?: OgConfig;
|
|
731
753
|
/** Generate robots.txt (with a Sitemap reference when available). Defaults to `true`. */
|
|
732
754
|
robots?: boolean;
|
|
733
755
|
/** RSS/Atom feeds. */
|
|
@@ -979,8 +1001,6 @@ export interface BlumeConfig {
|
|
|
979
1001
|
logo?: LogoConfig;
|
|
980
1002
|
/** Markdown / MDX rendering behavior. */
|
|
981
1003
|
markdown?: MarkdownConfig;
|
|
982
|
-
/** Expose the docs as an MCP server for agents. */
|
|
983
|
-
mcp?: McpConfig;
|
|
984
1004
|
/** Header, sidebar, tabs, and switchers. */
|
|
985
1005
|
navigation?: NavigationConfig;
|
|
986
1006
|
/** Native OpenAPI reference. */
|
package/src/core/config.ts
CHANGED
|
@@ -74,9 +74,9 @@ import type { Diagnostic } from "./types.ts";
|
|
|
74
74
|
* `algolia`, `typesense`, `orama-cloud`, `mixedbread`, or `none`) plus its
|
|
75
75
|
* credential block.
|
|
76
76
|
* - `ai` — `ask` (the Ask AI chat endpoint and its provider/model), `llmsTxt`
|
|
77
|
-
* (emit `llms.txt`),
|
|
78
|
-
*
|
|
79
|
-
* -
|
|
77
|
+
* (emit `llms.txt`), `mcp` (expose the docs as an MCP server for connecting
|
|
78
|
+
* agents), and `markdownComponents` (Markdown serializers for custom
|
|
79
|
+
* components in agent-facing output).
|
|
80
80
|
*
|
|
81
81
|
* **SEO, feeds & analytics**
|
|
82
82
|
* - `seo` — `og` images, `sitemap`, `robots`, `rss` feeds, `structuredData`
|
package/src/core/data.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface BlumeLogo {
|
|
|
15
15
|
svg?: string;
|
|
16
16
|
light?: string;
|
|
17
17
|
dark?: string;
|
|
18
|
+
dimensions?: {
|
|
19
|
+
dark?: { height: number; width: number };
|
|
20
|
+
light?: { height: number; width: number };
|
|
21
|
+
};
|
|
18
22
|
alt: string;
|
|
19
23
|
href: string;
|
|
20
24
|
/** Wordmark text beside the mark; `undefined` falls back to the site title. */
|
|
@@ -109,7 +113,11 @@ export interface BlumeDataConfig {
|
|
|
109
113
|
/** Hosted MCP server, or `null` when MCP is off. */
|
|
110
114
|
mcp: { name: string; route: string } | null;
|
|
111
115
|
/** Open Graph image generation. */
|
|
112
|
-
og: {
|
|
116
|
+
og: {
|
|
117
|
+
enabled: boolean;
|
|
118
|
+
logo?: string;
|
|
119
|
+
palette?: ResolvedConfig["seo"]["og"]["palette"];
|
|
120
|
+
};
|
|
113
121
|
/** Repository URL for header/edit links, or `null`. */
|
|
114
122
|
repoUrl: string | null;
|
|
115
123
|
search: { enabled: boolean; provider: SearchProvider };
|
package/src/core/navigation.ts
CHANGED
|
@@ -433,6 +433,43 @@ const buildConfigSidebar = (
|
|
|
433
433
|
return nodes;
|
|
434
434
|
};
|
|
435
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Resolve a tab's clickable target. A tab's `path` scopes its sidebar section
|
|
438
|
+
* but need not be a real route — a section with no index page would 404 if the
|
|
439
|
+
* tab linked straight to it. Prefer an exact page/group at the path; otherwise
|
|
440
|
+
* fall back to the first linkable route in the section (sidebar order).
|
|
441
|
+
*/
|
|
442
|
+
const resolveTabHref = (sidebar: NavNode[], path: string): string => {
|
|
443
|
+
let first: string | undefined;
|
|
444
|
+
const walk = (nodes: NavNode[]): boolean => {
|
|
445
|
+
for (const node of nodes) {
|
|
446
|
+
const { route } = node;
|
|
447
|
+
if (route === path) {
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
if (
|
|
451
|
+
first === undefined &&
|
|
452
|
+
route !== undefined &&
|
|
453
|
+
route.startsWith(`${path}/`)
|
|
454
|
+
) {
|
|
455
|
+
first = route;
|
|
456
|
+
}
|
|
457
|
+
if (node.kind === "group" && walk(node.children)) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
};
|
|
463
|
+
return walk(sidebar) ? path : (first ?? path);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
/** Attach a resolved `href` to each tab whose section has no index page. */
|
|
467
|
+
const withTabHrefs = (tabs: NavTab[], sidebar: NavNode[]): NavTab[] =>
|
|
468
|
+
tabs.map((tab) => {
|
|
469
|
+
const href = resolveTabHref(sidebar, tab.path);
|
|
470
|
+
return href === tab.path ? tab : { ...tab, href };
|
|
471
|
+
});
|
|
472
|
+
|
|
436
473
|
/** Build the complete navigation model from pages, meta, and config. */
|
|
437
474
|
export const buildNavigation = (
|
|
438
475
|
pages: PageRecord[],
|
|
@@ -519,11 +556,17 @@ export const buildNavigation = (
|
|
|
519
556
|
}
|
|
520
557
|
|
|
521
558
|
if (options.sidebar) {
|
|
559
|
+
const sidebar = buildConfigSidebar(
|
|
560
|
+
options.sidebar,
|
|
561
|
+
byRoute,
|
|
562
|
+
display,
|
|
563
|
+
basePath
|
|
564
|
+
);
|
|
522
565
|
return {
|
|
523
566
|
featured,
|
|
524
567
|
selectors,
|
|
525
|
-
sidebar
|
|
526
|
-
tabs,
|
|
568
|
+
sidebar,
|
|
569
|
+
tabs: withTabHrefs(tabs, sidebar),
|
|
527
570
|
};
|
|
528
571
|
}
|
|
529
572
|
|
|
@@ -534,19 +577,18 @@ export const buildNavigation = (
|
|
|
534
577
|
// prefix (`/docs`, `/fr`) and a bare `"/"` check would miss the match (or,
|
|
535
578
|
// under a base, falsely scope a group named like the prefix).
|
|
536
579
|
const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
|
|
580
|
+
const sidebar = buildFileSystemSidebar(
|
|
581
|
+
pages,
|
|
582
|
+
options.folderMeta,
|
|
583
|
+
sharedFolderMeta,
|
|
584
|
+
metaPrefix,
|
|
585
|
+
display,
|
|
586
|
+
new Set(tabs.flatMap((tab) => (tab.path === rootTabPath ? [] : [tab.path])))
|
|
587
|
+
);
|
|
537
588
|
return {
|
|
538
589
|
featured,
|
|
539
590
|
selectors,
|
|
540
|
-
sidebar
|
|
541
|
-
|
|
542
|
-
options.folderMeta,
|
|
543
|
-
sharedFolderMeta,
|
|
544
|
-
metaPrefix,
|
|
545
|
-
display,
|
|
546
|
-
new Set(
|
|
547
|
-
tabs.flatMap((tab) => (tab.path === rootTabPath ? [] : [tab.path]))
|
|
548
|
-
)
|
|
549
|
-
),
|
|
550
|
-
tabs,
|
|
591
|
+
sidebar,
|
|
592
|
+
tabs: withTabHrefs(tabs, sidebar),
|
|
551
593
|
};
|
|
552
594
|
};
|
package/src/core/schema.ts
CHANGED
|
@@ -550,6 +550,19 @@ export const askAiProviders = [
|
|
|
550
550
|
"openai-compatible",
|
|
551
551
|
] as const;
|
|
552
552
|
|
|
553
|
+
const mcpConfigSchema = z.strictObject({
|
|
554
|
+
enabled: z.boolean().default(false),
|
|
555
|
+
/** Optional system hint passed to connecting agents. */
|
|
556
|
+
instructions: z.string().optional(),
|
|
557
|
+
/** Server name shown to clients; defaults to the site title. */
|
|
558
|
+
name: z.string().optional(),
|
|
559
|
+
/**
|
|
560
|
+
* Normalized like `openapi.route`: a slash-less value would otherwise be
|
|
561
|
+
* string-concatenated onto the site origin (`https://acme.comdocs-mcp`).
|
|
562
|
+
*/
|
|
563
|
+
route: z.string().default("/mcp").transform(normalizeRoute),
|
|
564
|
+
});
|
|
565
|
+
|
|
553
566
|
const aiConfigSchema = z.strictObject({
|
|
554
567
|
ask: z
|
|
555
568
|
.strictObject({
|
|
@@ -620,6 +633,8 @@ const aiConfigSchema = z.strictObject({
|
|
|
620
633
|
})
|
|
621
634
|
)
|
|
622
635
|
.default({}),
|
|
636
|
+
/** Expose the docs as an MCP server for connecting agents. */
|
|
637
|
+
mcp: mcpConfigSchema.default({}),
|
|
623
638
|
});
|
|
624
639
|
|
|
625
640
|
/**
|
|
@@ -657,7 +672,7 @@ const navigationConfigSchema = z.strictObject({
|
|
|
657
672
|
.transform((value) =>
|
|
658
673
|
Array.isArray(value) ? { display: "flat" as const, items: value } : value
|
|
659
674
|
),
|
|
660
|
-
tabs: z.array(navTabSchema).
|
|
675
|
+
tabs: z.array(navTabSchema).default([]),
|
|
661
676
|
});
|
|
662
677
|
|
|
663
678
|
export type AskAiProvider = (typeof askAiProviders)[number];
|
|
@@ -679,19 +694,6 @@ const exportConfigSchema = z
|
|
|
679
694
|
typeof value === "boolean" ? { epub: value, pdf: value } : value
|
|
680
695
|
);
|
|
681
696
|
|
|
682
|
-
const mcpConfigSchema = z.strictObject({
|
|
683
|
-
enabled: z.boolean().default(false),
|
|
684
|
-
/** Optional system hint passed to connecting agents. */
|
|
685
|
-
instructions: z.string().optional(),
|
|
686
|
-
/** Server name shown to clients; defaults to the site title. */
|
|
687
|
-
name: z.string().optional(),
|
|
688
|
-
/**
|
|
689
|
-
* Normalized like `openapi.route`: a slash-less value would otherwise be
|
|
690
|
-
* string-concatenated onto the site origin (`https://acme.comdocs-mcp`).
|
|
691
|
-
*/
|
|
692
|
-
route: z.string().default("/mcp").transform(normalizeRoute),
|
|
693
|
-
});
|
|
694
|
-
|
|
695
697
|
/** A configured locale: ISO-ish code plus display metadata for the switcher. */
|
|
696
698
|
const localeSchema = z.strictObject({
|
|
697
699
|
code: z.string().min(1),
|
|
@@ -800,6 +802,18 @@ const xConfigSchema = z.strictObject({
|
|
|
800
802
|
handle: xHandleSchema,
|
|
801
803
|
});
|
|
802
804
|
|
|
805
|
+
const ogColorSchema = z
|
|
806
|
+
.string()
|
|
807
|
+
.regex(/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/iu);
|
|
808
|
+
|
|
809
|
+
const ogPaletteSchema = z.strictObject({
|
|
810
|
+
accent: ogColorSchema.optional(),
|
|
811
|
+
background: ogColorSchema.optional(),
|
|
812
|
+
border: ogColorSchema.optional(),
|
|
813
|
+
foreground: ogColorSchema.optional(),
|
|
814
|
+
muted: ogColorSchema.optional(),
|
|
815
|
+
});
|
|
816
|
+
|
|
803
817
|
const ogConfigSchema = z.strictObject({
|
|
804
818
|
/**
|
|
805
819
|
* Generate a per-page Open Graph image. Defaults to on once a deployment
|
|
@@ -808,6 +822,10 @@ const ogConfigSchema = z.strictObject({
|
|
|
808
822
|
* `loadConfig`. An explicit value here always wins.
|
|
809
823
|
*/
|
|
810
824
|
enabled: z.boolean().optional(),
|
|
825
|
+
/** Local SVG used in the generated card instead of the site logo. */
|
|
826
|
+
logo: z.string().optional(),
|
|
827
|
+
/** Optional generated-card colors. */
|
|
828
|
+
palette: ogPaletteSchema.optional(),
|
|
811
829
|
});
|
|
812
830
|
|
|
813
831
|
const rssConfigSchema = z.strictObject({
|
|
@@ -1088,7 +1106,6 @@ export const blumeConfigSchema = z.strictObject({
|
|
|
1088
1106
|
lastModified: lastModifiedConfigSchema.default(false),
|
|
1089
1107
|
logo: logoConfigSchema.optional(),
|
|
1090
1108
|
markdown: markdownConfigSchema.default({}),
|
|
1091
|
-
mcp: mcpConfigSchema.default({}),
|
|
1092
1109
|
navigation: navigationConfigSchema.default({}),
|
|
1093
1110
|
openapi: openapiConfigSchema.default({}),
|
|
1094
1111
|
react: reactConfigSchema.default({}),
|
|
@@ -11,7 +11,7 @@ export const serverFeatures = (config: ResolvedConfig): string[] => {
|
|
|
11
11
|
features.push("Ask AI");
|
|
12
12
|
}
|
|
13
13
|
// The hosted MCP server is a live JSON-RPC endpoint, so it needs a runtime.
|
|
14
|
-
if (config.mcp.enabled) {
|
|
14
|
+
if (config.ai.mcp.enabled) {
|
|
15
15
|
features.push("MCP server");
|
|
16
16
|
}
|
|
17
17
|
// Mixedbread (and any future provider) that proxies queries through a secret
|
|
@@ -23,6 +23,11 @@ import type { WatchListener } from "node:fs";
|
|
|
23
23
|
*/
|
|
24
24
|
export const BLUME_IGNORE_DIRS = [
|
|
25
25
|
".blume",
|
|
26
|
+
// The isolated `blume check --isolated` runtime. A sibling of `.blume`, it is
|
|
27
|
+
// written while a dev server runs; without this the content-layer watcher (or
|
|
28
|
+
// a `.`-rooted fs.watch) would treat its generation as a content change and
|
|
29
|
+
// reload — the very thing `--isolated` promises not to do.
|
|
30
|
+
".blume-verify",
|
|
26
31
|
".cache",
|
|
27
32
|
".git",
|
|
28
33
|
".next",
|
package/src/core/types.ts
CHANGED
|
@@ -159,7 +159,17 @@ export type NavNode =
|
|
|
159
159
|
/** Top-level tab/section. */
|
|
160
160
|
export interface NavTab {
|
|
161
161
|
label: string;
|
|
162
|
+
/**
|
|
163
|
+
* The tab's section prefix, used to scope the sidebar and match the active
|
|
164
|
+
* tab. Not necessarily a linkable route — a section may have no index page.
|
|
165
|
+
*/
|
|
162
166
|
path: string;
|
|
167
|
+
/**
|
|
168
|
+
* The clickable target. Equals `path` when the section has an index page;
|
|
169
|
+
* otherwise it's resolved to the section's first page so the tab never links
|
|
170
|
+
* to a 404. Absent when it matches `path`.
|
|
171
|
+
*/
|
|
172
|
+
href?: string;
|
|
163
173
|
icon?: string;
|
|
164
174
|
items?: NavSelectorItem[];
|
|
165
175
|
}
|
|
@@ -83,7 +83,17 @@ export const surfaceAdapterOutput = async (
|
|
|
83
83
|
}
|
|
84
84
|
await mkdir(dirname(to), { recursive: true });
|
|
85
85
|
await rm(to, { force: true, recursive: true });
|
|
86
|
-
|
|
86
|
+
// `verbatimSymlinks` keeps each symlink's target text as written. Without it,
|
|
87
|
+
// `cp` resolves every relative target against the *source*, rewriting it to an
|
|
88
|
+
// absolute path under `.blume` — which this function then deletes. Adapters
|
|
89
|
+
// that trace dependencies into their function bundle link one package to
|
|
90
|
+
// another that way (under an isolated linker — Bun's `isolated` mode, pnpm —
|
|
91
|
+
// that is every external dependency the function imports), so the resolved
|
|
92
|
+
// links leave the deployed function dying on its first external import with
|
|
93
|
+
// ERR_MODULE_NOT_FOUND. Verbatim, the links stay relative and internal to the
|
|
94
|
+
// bundle, surviving both this move and the platform's own (Vercel mounts the
|
|
95
|
+
// bundle at `/var/task`).
|
|
96
|
+
await cp(from, to, { recursive: true, verbatimSymlinks: true });
|
|
87
97
|
await rm(from, { force: true, recursive: true });
|
|
88
98
|
// The `.gitignore` entry is the surfaced top-level dir (`.vercel`/`.netlify`),
|
|
89
99
|
// never the moved sub-path — the platform's own state (`.vercel/project.json`,
|
package/src/markdown/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { languageIconTransformer } from "./language-icon.ts";
|
|
|
17
17
|
import { mathPlugin } from "./math.ts";
|
|
18
18
|
import { mermaidPlugin } from "./mermaid.ts";
|
|
19
19
|
import { packageInstallPlugin } from "./package-install.ts";
|
|
20
|
+
import { tableWrapPlugin } from "./table-wrap.ts";
|
|
20
21
|
import { DEFAULT_CODE_THEMES } from "./themes.ts";
|
|
21
22
|
import type { CodeThemes } from "./themes.ts";
|
|
22
23
|
|
|
@@ -58,6 +59,7 @@ type HastPlugin = NonNullable<
|
|
|
58
59
|
const blumeHastPlugins = (options: BlumeMarkdownOptions): HastPlugin[] => {
|
|
59
60
|
const plugins: HastPlugin[] = [
|
|
60
61
|
inlineCodeHighlightPlugin(options.codeThemes) as unknown as HastPlugin,
|
|
62
|
+
tableWrapPlugin() as unknown as HastPlugin,
|
|
61
63
|
];
|
|
62
64
|
if (options.headingAnchors !== false) {
|
|
63
65
|
plugins.push(headingAnchorPlugin() as unknown as HastPlugin);
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* hex colors are skipped because dark-on-dark logos (Next.js, Rust…) vanish.
|
|
8
8
|
*
|
|
9
9
|
* The theme styles `.blume-lang-icon` and shifts the language label
|
|
10
|
-
* (`pre[data-icon]::before`) to make room
|
|
10
|
+
* (`pre[data-language][data-icon]::before`) to make room — gated on
|
|
11
|
+
* `data-language` so the icon only shows when a header bar exists to hold it.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import {
|