blume 1.1.0 → 1.1.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 +24 -0
- package/dist/cli/index.js +319 -91
- package/dist/cli/index.js.map +18 -18
- package/dist/types/ai/component-markdown.d.ts +12 -1
- package/dist/types/core/config-input.d.ts +18 -8
- package/dist/types/core/schema.d.ts +38 -24
- package/dist/types/core/types.d.ts +14 -0
- package/dist/types/markdown/themes.d.ts +21 -0
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/configuration/ai.mdx +2 -2
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/seo.mdx +16 -0
- package/docs/content/components.mdx +1 -1
- package/docs/content/navigation.mdx +4 -0
- package/docs/content/syntax.mdx +14 -0
- package/docs/reference/cli.mdx +1 -1
- package/docs/reference/frontmatter.mdx +2 -0
- package/package.json +2 -1
- package/src/ai/component-markdown.ts +39 -11
- package/src/ai/llms.ts +4 -2
- package/src/ai/markdown.ts +5 -1
- package/src/astro/generate.ts +121 -32
- package/src/astro/pages.ts +21 -5
- package/src/astro/templates.ts +70 -7
- package/src/audit/checks/llms.ts +4 -1
- package/src/cli/commands/build.ts +13 -1
- package/src/cli/prepare.ts +10 -2
- package/src/components/content/Component.astro +99 -6
- package/src/components/content/diff.ts +53 -4
- package/src/components/layout/RootLayout.astro +9 -1
- package/src/components/layout/nav-utils.ts +18 -7
- package/src/core/config-input.ts +18 -8
- package/src/core/diagnostics.ts +2 -0
- package/src/core/graph.ts +23 -4
- package/src/core/navigation.ts +180 -21
- package/src/core/project-graph.ts +54 -28
- package/src/core/schema.ts +39 -2
- package/src/core/sources/github-releases.ts +65 -2
- package/src/core/types.ts +14 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/inline-code.ts +1 -1
- package/src/markdown/themes.ts +7 -2
- package/src/markdown/twoslash.ts +60 -0
- package/src/registry/eject.ts +3 -1
- /package/docs/{03-faq.mdx → 07-faq.mdx} +0 -0
|
@@ -14,6 +14,12 @@ export interface ComponentMarkdownContext extends EvaluatedProps {
|
|
|
14
14
|
childComponents: (name: string) => ComponentMarkdownChild[];
|
|
15
15
|
/** The element's body, downleveled and dedented (empty if self-closing). */
|
|
16
16
|
children: string;
|
|
17
|
+
/**
|
|
18
|
+
* The page's parsed front-matter (empty when the caller has none). Lets a
|
|
19
|
+
* serializer read page metadata directly, even when a prop expression is
|
|
20
|
+
* not statically evaluable.
|
|
21
|
+
*/
|
|
22
|
+
frontmatter: Record<string, unknown>;
|
|
17
23
|
}
|
|
18
24
|
/**
|
|
19
25
|
* A component's Markdown serializer. Return the replacement Markdown, or
|
|
@@ -29,6 +35,11 @@ export type ComponentMarkdown = (context: ComponentMarkdownContext) => string |
|
|
|
29
35
|
* `components` adds user serializers from `ai.markdownComponents`, layered
|
|
30
36
|
* over the built-ins: a same-name entry replaces the built-in serializer, and
|
|
31
37
|
* one that always returns `null` effectively opts that component out.
|
|
38
|
+
*
|
|
39
|
+
* `frontmatter` is the page's parsed front-matter data. It is put in scope
|
|
40
|
+
* when evaluating attribute expressions — so `prop={frontmatter.status}`
|
|
41
|
+
* resolves the way it does when Astro renders the page — and handed to
|
|
42
|
+
* serializers on their context.
|
|
32
43
|
*/
|
|
33
|
-
export declare const downlevelComponents: (source: string, components?: Record<string, ComponentMarkdown>) => string;
|
|
44
|
+
export declare const downlevelComponents: (source: string, components?: Record<string, ComponentMarkdown>, frontmatter?: Record<string, unknown>) => string;
|
|
34
45
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentMarkdown } from "../ai/component-markdown.ts";
|
|
2
|
+
import type { CodeTheme } from "../markdown/themes.ts";
|
|
2
3
|
import type { FontSlug } from "../theme/fonts.ts";
|
|
3
4
|
import type { OpenApiSource, SearchProvider, SidebarDisplay, SidebarItemConfig } from "./schema.ts";
|
|
4
5
|
import type { ContentSource } from "./sources/types.ts";
|
|
@@ -487,9 +488,11 @@ export interface AiConfig {
|
|
|
487
488
|
/**
|
|
488
489
|
* Markdown serializers for custom components in agent-facing output (the
|
|
489
490
|
* `.md` mirror, `llms-full.txt`, MCP `get_page`), keyed by JSX name. Each
|
|
490
|
-
* receives the component's statically-evaluated `props`
|
|
491
|
-
* `
|
|
492
|
-
*
|
|
491
|
+
* receives the component's statically-evaluated `props` (with the page's
|
|
492
|
+
* `frontmatter` in scope, so `prop={frontmatter.status}` resolves), its
|
|
493
|
+
* downleveled `children`, and the page's `frontmatter` data, and returns
|
|
494
|
+
* replacement Markdown — or `null` to leave the JSX verbatim. A same-name
|
|
495
|
+
* entry replaces a built-in serializer.
|
|
493
496
|
*
|
|
494
497
|
* These live in `blume.config.ts` (which is executed at build time), not in
|
|
495
498
|
* `components.tsx` (which is only statically analyzed, never run).
|
|
@@ -654,6 +657,13 @@ export interface OgConfig {
|
|
|
654
657
|
logo?: string;
|
|
655
658
|
/** Optional generated-card colors. */
|
|
656
659
|
palette?: OgPaletteConfig;
|
|
660
|
+
/**
|
|
661
|
+
* Card headlines for custom `.astro` pages, keyed by route (`"/"`, `"/cli"`).
|
|
662
|
+
* A custom page has no frontmatter to read, so its card is otherwise titled
|
|
663
|
+
* by humanizing its last URL segment (`/cli` → "Cli"); an entry here wins.
|
|
664
|
+
* Content pages always take their card headline from the page title.
|
|
665
|
+
*/
|
|
666
|
+
titles?: Record<string, string>;
|
|
657
667
|
}
|
|
658
668
|
/** Discoverability: OG images, feeds, sitemap, robots, and structured data. */
|
|
659
669
|
export interface SeoConfig {
|
|
@@ -713,12 +723,12 @@ export interface MarkdownConfig {
|
|
|
713
723
|
* `` `code`{:lang} ``, `<CodeBlock>`, and `<Diff>`.
|
|
714
724
|
*/
|
|
715
725
|
codeBlocks?: {
|
|
716
|
-
/** Shiki theme names per color mode. */
|
|
726
|
+
/** Bundled Shiki theme names or inline custom Shiki themes per color mode. */
|
|
717
727
|
theme?: {
|
|
718
|
-
/** Dark-mode theme. Defaults to `github-dark`. */
|
|
719
|
-
dark?:
|
|
720
|
-
/** Light-mode theme. Defaults to `github-light`. */
|
|
721
|
-
light?:
|
|
728
|
+
/** Dark-mode theme name or custom theme. Defaults to `github-dark`. */
|
|
729
|
+
dark?: CodeTheme;
|
|
730
|
+
/** Light-mode theme name or custom theme. Defaults to `github-light`. */
|
|
731
|
+
light?: CodeTheme;
|
|
722
732
|
};
|
|
723
733
|
};
|
|
724
734
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ComponentMarkdown } from "../ai/component-markdown.ts";
|
|
3
|
+
import type { CodeTheme } from "../markdown/themes.ts";
|
|
3
4
|
import type { ContentSource } from "./sources/types.ts";
|
|
4
5
|
import type { StandardSchema } from "./standard-schema.ts";
|
|
5
6
|
declare const hydrationMode: z.ZodEnum<["load", "idle", "visible", "media", "only"]>;
|
|
@@ -1916,24 +1917,24 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1916
1917
|
}>>;
|
|
1917
1918
|
codeBlocks: z.ZodDefault<z.ZodObject<{
|
|
1918
1919
|
theme: z.ZodDefault<z.ZodObject<{
|
|
1919
|
-
dark: z.ZodDefault<z.
|
|
1920
|
-
light: z.ZodDefault<z.
|
|
1920
|
+
dark: z.ZodDefault<z.ZodType<CodeTheme, z.ZodTypeDef, CodeTheme>>;
|
|
1921
|
+
light: z.ZodDefault<z.ZodType<CodeTheme, z.ZodTypeDef, CodeTheme>>;
|
|
1921
1922
|
}, "strict", z.ZodTypeAny, {
|
|
1922
|
-
dark:
|
|
1923
|
-
light:
|
|
1923
|
+
dark: CodeTheme;
|
|
1924
|
+
light: CodeTheme;
|
|
1924
1925
|
}, {
|
|
1925
|
-
dark?:
|
|
1926
|
-
light?:
|
|
1926
|
+
dark?: CodeTheme | undefined;
|
|
1927
|
+
light?: CodeTheme | undefined;
|
|
1927
1928
|
}>>;
|
|
1928
1929
|
}, "strict", z.ZodTypeAny, {
|
|
1929
1930
|
theme: {
|
|
1930
|
-
dark:
|
|
1931
|
-
light:
|
|
1931
|
+
dark: CodeTheme;
|
|
1932
|
+
light: CodeTheme;
|
|
1932
1933
|
};
|
|
1933
1934
|
}, {
|
|
1934
1935
|
theme?: {
|
|
1935
|
-
dark?:
|
|
1936
|
-
light?:
|
|
1936
|
+
dark?: CodeTheme | undefined;
|
|
1937
|
+
light?: CodeTheme | undefined;
|
|
1937
1938
|
} | undefined;
|
|
1938
1939
|
}>>;
|
|
1939
1940
|
/**
|
|
@@ -1954,8 +1955,8 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1954
1955
|
};
|
|
1955
1956
|
codeBlocks: {
|
|
1956
1957
|
theme: {
|
|
1957
|
-
dark:
|
|
1958
|
-
light:
|
|
1958
|
+
dark: CodeTheme;
|
|
1959
|
+
light: CodeTheme;
|
|
1959
1960
|
};
|
|
1960
1961
|
};
|
|
1961
1962
|
headingAnchors: boolean;
|
|
@@ -1967,8 +1968,8 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1967
1968
|
} | undefined;
|
|
1968
1969
|
codeBlocks?: {
|
|
1969
1970
|
theme?: {
|
|
1970
|
-
dark?:
|
|
1971
|
-
light?:
|
|
1971
|
+
dark?: CodeTheme | undefined;
|
|
1972
|
+
light?: CodeTheme | undefined;
|
|
1972
1973
|
} | undefined;
|
|
1973
1974
|
} | undefined;
|
|
1974
1975
|
headingAnchors?: boolean | undefined;
|
|
@@ -2529,6 +2530,13 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2529
2530
|
foreground?: string | undefined;
|
|
2530
2531
|
muted?: string | undefined;
|
|
2531
2532
|
}>>;
|
|
2533
|
+
/**
|
|
2534
|
+
* Card headlines for custom `.astro` pages, keyed by route (`"/"`, `"/cli"`).
|
|
2535
|
+
* A custom page has no frontmatter to read, so its card is otherwise titled
|
|
2536
|
+
* by humanizing its last URL segment (`/cli` → "Cli"); an entry here wins.
|
|
2537
|
+
* Content pages always take their card headline from the page title.
|
|
2538
|
+
*/
|
|
2539
|
+
titles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
2532
2540
|
}, "strict", z.ZodTypeAny, {
|
|
2533
2541
|
enabled?: boolean | undefined;
|
|
2534
2542
|
logo?: string | undefined;
|
|
@@ -2544,6 +2552,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2544
2552
|
foreground?: string | undefined;
|
|
2545
2553
|
muted?: string | undefined;
|
|
2546
2554
|
} | undefined;
|
|
2555
|
+
titles?: Record<string, string> | undefined;
|
|
2547
2556
|
}, {
|
|
2548
2557
|
enabled?: boolean | undefined;
|
|
2549
2558
|
logo?: string | undefined;
|
|
@@ -2559,6 +2568,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2559
2568
|
foreground?: string | undefined;
|
|
2560
2569
|
muted?: string | undefined;
|
|
2561
2570
|
} | undefined;
|
|
2571
|
+
titles?: Record<string, string> | undefined;
|
|
2562
2572
|
}>>;
|
|
2563
2573
|
/** Generate robots.txt (with a Sitemap reference when available). */
|
|
2564
2574
|
robots: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -2616,6 +2626,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2616
2626
|
foreground?: string | undefined;
|
|
2617
2627
|
muted?: string | undefined;
|
|
2618
2628
|
} | undefined;
|
|
2629
|
+
titles?: Record<string, string> | undefined;
|
|
2619
2630
|
};
|
|
2620
2631
|
robots: boolean;
|
|
2621
2632
|
rss: {
|
|
@@ -2651,6 +2662,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2651
2662
|
foreground?: string | undefined;
|
|
2652
2663
|
muted?: string | undefined;
|
|
2653
2664
|
} | undefined;
|
|
2665
|
+
titles?: Record<string, string> | undefined;
|
|
2654
2666
|
} | undefined;
|
|
2655
2667
|
robots?: boolean | undefined;
|
|
2656
2668
|
rss?: {
|
|
@@ -2803,6 +2815,9 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2803
2815
|
} | undefined>;
|
|
2804
2816
|
}, "strict", z.ZodTypeAny, {
|
|
2805
2817
|
title: string;
|
|
2818
|
+
frontmatter: {
|
|
2819
|
+
extend: Record<string, StandardSchema<unknown, unknown>>;
|
|
2820
|
+
};
|
|
2806
2821
|
openapi: {
|
|
2807
2822
|
enabled: boolean;
|
|
2808
2823
|
route: string;
|
|
@@ -2961,9 +2976,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2961
2976
|
pdf: boolean;
|
|
2962
2977
|
};
|
|
2963
2978
|
feedback: boolean;
|
|
2964
|
-
frontmatter: {
|
|
2965
|
-
extend: Record<string, StandardSchema<unknown, unknown>>;
|
|
2966
|
-
};
|
|
2967
2979
|
markdown: {
|
|
2968
2980
|
code: {
|
|
2969
2981
|
icons: boolean;
|
|
@@ -2971,8 +2983,8 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2971
2983
|
};
|
|
2972
2984
|
codeBlocks: {
|
|
2973
2985
|
theme: {
|
|
2974
|
-
dark:
|
|
2975
|
-
light:
|
|
2986
|
+
dark: CodeTheme;
|
|
2987
|
+
light: CodeTheme;
|
|
2976
2988
|
};
|
|
2977
2989
|
};
|
|
2978
2990
|
headingAnchors: boolean;
|
|
@@ -3074,6 +3086,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3074
3086
|
foreground?: string | undefined;
|
|
3075
3087
|
muted?: string | undefined;
|
|
3076
3088
|
} | undefined;
|
|
3089
|
+
titles?: Record<string, string> | undefined;
|
|
3077
3090
|
};
|
|
3078
3091
|
robots: boolean;
|
|
3079
3092
|
rss: {
|
|
@@ -3145,6 +3158,9 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3145
3158
|
} | undefined;
|
|
3146
3159
|
}, {
|
|
3147
3160
|
title?: string | undefined;
|
|
3161
|
+
frontmatter?: {
|
|
3162
|
+
extend?: Record<string, StandardSchema<unknown, unknown>> | undefined;
|
|
3163
|
+
} | undefined;
|
|
3148
3164
|
openapi?: {
|
|
3149
3165
|
enabled?: boolean | undefined;
|
|
3150
3166
|
route?: string | undefined;
|
|
@@ -3332,9 +3348,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3332
3348
|
pdf?: boolean | undefined;
|
|
3333
3349
|
} | undefined;
|
|
3334
3350
|
feedback?: boolean | undefined;
|
|
3335
|
-
frontmatter?: {
|
|
3336
|
-
extend?: Record<string, StandardSchema<unknown, unknown>> | undefined;
|
|
3337
|
-
} | undefined;
|
|
3338
3351
|
i18n?: {
|
|
3339
3352
|
locales: {
|
|
3340
3353
|
code: string;
|
|
@@ -3363,8 +3376,8 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3363
3376
|
} | undefined;
|
|
3364
3377
|
codeBlocks?: {
|
|
3365
3378
|
theme?: {
|
|
3366
|
-
dark?:
|
|
3367
|
-
light?:
|
|
3379
|
+
dark?: CodeTheme | undefined;
|
|
3380
|
+
light?: CodeTheme | undefined;
|
|
3368
3381
|
} | undefined;
|
|
3369
3382
|
} | undefined;
|
|
3370
3383
|
headingAnchors?: boolean | undefined;
|
|
@@ -3466,6 +3479,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3466
3479
|
foreground?: string | undefined;
|
|
3467
3480
|
muted?: string | undefined;
|
|
3468
3481
|
} | undefined;
|
|
3482
|
+
titles?: Record<string, string> | undefined;
|
|
3469
3483
|
} | undefined;
|
|
3470
3484
|
robots?: boolean | undefined;
|
|
3471
3485
|
rss?: {
|
|
@@ -103,6 +103,13 @@ export interface PageRecord {
|
|
|
103
103
|
* `/guides/x`). Pages with the same key are translations of each other.
|
|
104
104
|
*/
|
|
105
105
|
translationKey: string;
|
|
106
|
+
/**
|
|
107
|
+
* True for entries filled in from the fallback locale to pad a locale's
|
|
108
|
+
* navigation for pages it hasn't translated yet. The record's content —
|
|
109
|
+
* title included — belongs to the fallback locale, so per-locale content
|
|
110
|
+
* checks skip these.
|
|
111
|
+
*/
|
|
112
|
+
fallback?: boolean;
|
|
106
113
|
/**
|
|
107
114
|
* Content-relative path with the leading locale directory stripped, used for
|
|
108
115
|
* sidebar grouping so the locale dir is not surfaced as a nav group. Equals
|
|
@@ -206,6 +213,13 @@ export interface Navigation {
|
|
|
206
213
|
tabs: NavTab[];
|
|
207
214
|
selectors: NavSelector[];
|
|
208
215
|
sidebar: NavNode[];
|
|
216
|
+
/**
|
|
217
|
+
* The tree root in final path space — localized and based (`/`, `/en`,
|
|
218
|
+
* `/docs`). Tab paths arrive in the same space, so the tab sitting at this
|
|
219
|
+
* path spans the whole tree and must be scoped as the root tab, not as a
|
|
220
|
+
* section tab. Absent on older serialized graphs; treat as `/`.
|
|
221
|
+
*/
|
|
222
|
+
root?: string;
|
|
209
223
|
/** Pinned links shown above the sidebar sections, unscoped by tab. */
|
|
210
224
|
featured: FeaturedLink[];
|
|
211
225
|
/** Repo URL for the header link, or null when hidden (`navigation.repo`). */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The light/dark Shiki themes Blume highlights code with. Every Shiki surface —
|
|
3
|
+
* fenced code (the generated Astro `shikiConfig.themes`), inline `` `code`{:lang} ``,
|
|
4
|
+
* out-of-pipeline `highlightCode`, and `<Diff>` — resolves to the same pair so a
|
|
5
|
+
* project's `markdown.codeBlocks.theme` shifts them all in lockstep. This is the
|
|
6
|
+
* single home for the github fallback used when nothing is configured.
|
|
7
|
+
*/
|
|
8
|
+
import type { ThemeRegistrationAny } from "shiki";
|
|
9
|
+
/** A bundled Shiki theme name or an inline custom Shiki theme definition. */
|
|
10
|
+
export type CodeTheme = string | ThemeRegistrationAny;
|
|
11
|
+
/**
|
|
12
|
+
* A light/dark Shiki theme pair (`markdown.codeBlocks.theme`). A `type` (not an
|
|
13
|
+
* `interface`) so it keeps the implicit index signature Shiki's `themes`
|
|
14
|
+
* parameter (`Partial<Record<string, …>>`) expects.
|
|
15
|
+
*/
|
|
16
|
+
export type CodeThemes = {
|
|
17
|
+
dark: CodeTheme;
|
|
18
|
+
light: CodeTheme;
|
|
19
|
+
};
|
|
20
|
+
/** The default pair, used when `markdown.codeBlocks.theme` is unset. */
|
|
21
|
+
export declare const DEFAULT_CODE_THEMES: CodeThemes;
|
|
@@ -87,7 +87,7 @@ content: {
|
|
|
87
87
|
}
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
The release name becomes the title, its tag becomes `changelog.version`, and its published date sorts the timeline. A private repo authenticates with the `GITHUB_TOKEN` environment variable. See [Content sources](/docs/content/sources#github-releases) for every option.
|
|
90
|
+
The release name becomes the title, its tag becomes `changelog.version`, and its published date sorts the timeline. Each release page also gets a unique meta description summarized from its notes — markdown stripped, section headings and changeset commit-hash prefixes dropped, trimmed to the search-snippet length [`blume audit`](/docs/reference/cli#auditing-the-built-site) checks for — instead of falling back to the site description. A private repo authenticates with the `GITHUB_TOKEN` environment variable. See [Content sources](/docs/content/sources#github-releases) for every option.
|
|
91
91
|
|
|
92
92
|
## The RSS feed
|
|
93
93
|
|
|
@@ -47,11 +47,11 @@ Append `.md` or `.mdx` to any page's URL to fetch its raw Markdown source — pe
|
|
|
47
47
|
|
|
48
48
|
Nested routes work the same way (`/content/syntax.md`), and the home page is served at `/index.md`.
|
|
49
49
|
|
|
50
|
-
The `.md` variant _downlevels_ components to plain Markdown for consumers that can't interpret JSX: `<TypeTable>` becomes a Markdown table, `<Callout>` a labeled blockquote, `<Steps>` an ordered list, `<Tabs>` bold-labeled sections, and `<YouTube>` a link. Anything that can't be converted faithfully — a custom component, or a prop computed from an import — is left as-is, and component markup inside fenced code blocks is never touched. The same conversion applies to `llms-full.txt` and the MCP server's `get_page` tool, so every agent-facing surface reads clean Markdown. When you want the untransformed source, use the `.mdx` variant.
|
|
50
|
+
The `.md` variant _downlevels_ components to plain Markdown for consumers that can't interpret JSX: `<TypeTable>` becomes a Markdown table, `<Callout>` a labeled blockquote, `<Steps>` an ordered list, `<Tabs>` bold-labeled sections, and `<YouTube>` a link. Props are evaluated with the page's `frontmatter` in scope, so a prop like `title={frontmatter.status}` resolves to the same value the rendered page shows. Anything that can't be converted faithfully — a custom component, or a prop computed from an import — is left as-is, and component markup inside fenced code blocks is never touched. The same conversion applies to `llms-full.txt` and the MCP server's `get_page` tool, so every agent-facing surface reads clean Markdown. When you want the untransformed source, use the `.mdx` variant.
|
|
51
51
|
|
|
52
52
|
### Custom component serializers
|
|
53
53
|
|
|
54
|
-
Give your own components a Markdown form with `ai.markdownComponents` — a map of JSX name to serializer. Each serializer receives the component's `props` (statically evaluated from the MDX attributes)
|
|
54
|
+
Give your own components a Markdown form with `ai.markdownComponents` — a map of JSX name to serializer. Each serializer receives the component's `props` (statically evaluated from the MDX attributes, with the page's `frontmatter` in scope), its `children` (already downleveled to Markdown), and the page's `frontmatter` data, and returns the replacement — or `null` to leave the JSX as-is:
|
|
55
55
|
|
|
56
56
|
```ts blume.config.ts lineNumbers
|
|
57
57
|
import { defineConfig } from "blume";
|
|
@@ -166,6 +166,22 @@ seo: {
|
|
|
166
166
|
|
|
167
167
|
Each entry is a family name, or an object pinning its `weight` (a number, a list, or a variable range like `"100..900"`) and `style` (`"normal"`, `"italic"`, or both). The families are fetched from Google Fonts at build — so a build that uses them needs network access — and the renderer only pulls the glyph subsets each title actually uses. Fallback is per-glyph, so adding a family only affects glyphs the built-in font can't draw; Latin text renders exactly as before.
|
|
168
168
|
|
|
169
|
+
### Custom page titles
|
|
170
|
+
|
|
171
|
+
A custom [`.astro` page](/docs/advanced/custom-pages) has no frontmatter to read, so its generated card is titled by humanizing the last URL segment of its route — `/getting-started` becomes "Getting Started", but `/cli` becomes "Cli". Name those cards explicitly with `og.titles`, keyed by route (`"/"` addresses the home, whose card otherwise carries the site title):
|
|
172
|
+
|
|
173
|
+
```ts blume.config.ts lineNumbers
|
|
174
|
+
seo: {
|
|
175
|
+
og: {
|
|
176
|
+
titles: {
|
|
177
|
+
"/cli": "CLI",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Entries only apply to custom pages — a content page's card always takes its headline from the page title, so retitle those in frontmatter instead.
|
|
184
|
+
|
|
169
185
|
`seo.image` is frontmatter, so it only covers Markdown and MDX content. To give a custom [`.astro` page](/docs/advanced/custom-pages) its own social image — a marketing home or landing page, and the way to give the home page alone a bespoke share image — pass the `ogImage` prop to `PageLayout`.
|
|
170
186
|
|
|
171
187
|
## RSS feeds
|
|
@@ -560,7 +560,7 @@ A card linking to a GitHub repository with its live star and fork counts. Counts
|
|
|
560
560
|
|
|
561
561
|
`Component` renders an example file from your project's `examples/` directory as a live preview alongside its highlighted source, in tabs. Point it at a file with `path` — its location under `examples/`, without the extension (so `examples/counter.tsx` is `path="counter"`). React, Vue, Svelte, and Astro examples are all supported; framework examples hydrate, Astro ones render statically. It keeps the preview and the code in sync from a single file.
|
|
562
562
|
|
|
563
|
-
The preview renders in an isolated frame that the docs styles never reach — no prose margins, typography, or theme chrome bleed into your component. The frame gets Tailwind (preflight + utilities scanned from your example files and anything they import), Blume's design tokens so classes like `bg-background` follow the site palette by default, and it follows the site's light/dark toggle live.
|
|
563
|
+
The preview renders in an isolated frame that the docs styles never reach — no prose margins, typography, or theme chrome bleed into your component. The frame gets Tailwind (preflight + utilities scanned from your example files and anything they import), Blume's design tokens so classes like `bg-background` follow the site palette by default, and it follows the site's light/dark toggle live. The pane sizes itself to the rendered example — and keeps tracking it if the example grows or shrinks after load — with the Preview and Code tabs sharing one height so toggling them never shifts the page.
|
|
564
564
|
|
|
565
565
|
To style previews with your own design system — say, shadcn variables — point `examples.css` at a stylesheet. It's injected into every preview frame after Blume's defaults, so your tokens win. Don't `@import "tailwindcss"` in it; the frame already provides Tailwind. Both `.dark` and `[data-theme="dark"]` work for dark-mode overrides:
|
|
566
566
|
|
|
@@ -45,6 +45,8 @@ export default defineMeta({
|
|
|
45
45
|
|
|
46
46
|
See [Folder meta](/docs/content/meta) for every field and computing meta at scan time.
|
|
47
47
|
|
|
48
|
+
A folder's `meta.title` and its own `index` page's frontmatter `title` are resolved independently — translating one under i18n and forgetting the other renders a correct sidebar with a stale `<title>`/heading on the landing page itself. Blume reports a `BLUME_NAV_INDEX_TITLE_MISMATCH` warning when they diverge. Untranslated pages filled in from the fallback locale are exempt — their title belongs to the fallback locale, and the fix is translating the page, not editing its frontmatter.
|
|
49
|
+
|
|
48
50
|
To group pages _without_ adding a URL segment, use a parenthesized folder name — see [Pages](/docs/content#group-folders).
|
|
49
51
|
|
|
50
52
|
## Display modes
|
|
@@ -86,6 +88,8 @@ When the sidebar is generated, order is resolved highest priority first:
|
|
|
86
88
|
</Step>
|
|
87
89
|
</Steps>
|
|
88
90
|
|
|
91
|
+
Two siblings that land on the same explicit or numeric order fall back to alphabetical order between themselves — Blume reports a `BLUME_DUPLICATE_SIDEBAR_ORDER` warning so the tie doesn't go unnoticed.
|
|
92
|
+
|
|
89
93
|
## Hidden pages
|
|
90
94
|
|
|
91
95
|
Hide a page from the sidebar — and from previous/next pagination — while keeping it built and reachable by its URL:
|
package/docs/content/syntax.mdx
CHANGED
|
@@ -154,6 +154,20 @@ export default defineConfig({
|
|
|
154
154
|
});
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
You can also provide a custom [Shiki theme definition](https://shiki.style/guide/load-theme) directly. Import a VS Code-compatible theme JSON file (using an import attribute when your runtime requires one) and assign it to either color mode; bundled names and custom definitions can be mixed:
|
|
158
|
+
|
|
159
|
+
```ts blume.config.ts
|
|
160
|
+
import darkTheme from "./themes/acme-dark.json" with { type: "json" };
|
|
161
|
+
|
|
162
|
+
export default defineConfig({
|
|
163
|
+
markdown: {
|
|
164
|
+
codeBlocks: {
|
|
165
|
+
theme: { light: "github-light", dark: darkTheme },
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
157
171
|
### Line numbers
|
|
158
172
|
|
|
159
173
|
Append `lineNumbers` to render a line-number gutter — on its own or alongside a title:
|
package/docs/reference/cli.mdx
CHANGED
|
@@ -35,7 +35,7 @@ blume <command> [options]
|
|
|
35
35
|
- `blume dev --content-dir <dir>` — scan a different content folder without editing `blume.config.ts`.
|
|
36
36
|
- `blume dev --debug` — verbose Astro/Vite logging for troubleshooting.
|
|
37
37
|
- `blume dev --preview` / `blume build --preview` — include drafts and unpublished CMS content.
|
|
38
|
-
- `blume build --strict` —
|
|
38
|
+
- `blume build --no-strict` — build despite diagnostic errors. By default `blume build` fails (exit 1) on any error diagnostic, because pages that fail frontmatter validation are dropped from the output; with `--no-strict` the build succeeds and reports how many pages are missing. `blume dev --strict` opts dev into the same fail-fast behavior.
|
|
39
39
|
- `blume build --output static|server --adapter vercel|node|netlify|cloudflare --base /docs` — override the deployment output, adapter, and base path from `blume.config.ts`.
|
|
40
40
|
- `blume build --analyze` — print the client JavaScript bundle sizes (largest first) after the build.
|
|
41
41
|
- `blume build --budget-js <kb> --budget-css <kb>` — fail the build when total client JavaScript/CSS exceeds the budget, turning a performance target into a CI gate.
|
|
@@ -109,4 +109,6 @@ reviewedAt: 2026-06-20
|
|
|
109
109
|
|
|
110
110
|
Schemas are accepted through the [Standard Schema](https://standardschema.dev) interface, so Zod (whichever version your project installs), Valibot, and ArkType all work. Every declared key is validated on every page — absent ones included — so a required schema enforces the key site-wide; mark it `.optional()` to validate only where present. All other keys stay strictly validated, and built-in fields can't be redeclared.
|
|
111
111
|
|
|
112
|
+
A page that fails validation fails `blume build` with a diagnostic naming the file and key. With [`--no-strict`](/docs/reference/cli#common-flags), the build succeeds anyway and the failing pages are dropped from the output — the build summary reports how many.
|
|
113
|
+
|
|
112
114
|
Schemas are exported from `blume/schema` for editor and migration tooling.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blume",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Documentation that's fast, AI-ready, and zero-config.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -112,6 +112,7 @@
|
|
|
112
112
|
"tailwindcss": "^4",
|
|
113
113
|
"takumi-js": "^2.2.1",
|
|
114
114
|
"tinyglobby": "^0.2.10",
|
|
115
|
+
"twoslash": "^0.3.9",
|
|
115
116
|
"typescript": "^6.0.3",
|
|
116
117
|
"undici": "^8.6.0",
|
|
117
118
|
"zod": "^3.24.0"
|
|
@@ -63,6 +63,12 @@ export interface ComponentMarkdownContext extends EvaluatedProps {
|
|
|
63
63
|
childComponents: (name: string) => ComponentMarkdownChild[];
|
|
64
64
|
/** The element's body, downleveled and dedented (empty if self-closing). */
|
|
65
65
|
children: string;
|
|
66
|
+
/**
|
|
67
|
+
* The page's parsed front-matter (empty when the caller has none). Lets a
|
|
68
|
+
* serializer read page metadata directly, even when a prop expression is
|
|
69
|
+
* not statically evaluable.
|
|
70
|
+
*/
|
|
71
|
+
frontmatter: Record<string, unknown>;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -78,15 +84,22 @@ export type ComponentMarkdown = (
|
|
|
78
84
|
* Statically evaluate an MDX attribute expression (`prop={...}`). Component
|
|
79
85
|
* data props are object/array/number literals in practice; evaluation runs at
|
|
80
86
|
* build time over the author's own content — the same trust level as the MDX
|
|
81
|
-
* itself, which Astro compiles and executes.
|
|
82
|
-
*
|
|
87
|
+
* itself, which Astro compiles and executes. The page's `frontmatter` is in
|
|
88
|
+
* scope, mirroring what Astro provides an MDX body at render time, so
|
|
89
|
+
* `prop={frontmatter.status}` resolves; expressions that reference imports or
|
|
90
|
+
* other scope throw and report as not evaluable.
|
|
83
91
|
*/
|
|
84
|
-
const evaluateExpression = (
|
|
92
|
+
const evaluateExpression = (
|
|
93
|
+
raw: string,
|
|
94
|
+
frontmatter: Record<string, unknown> | undefined
|
|
95
|
+
): { ok: boolean; value: unknown } => {
|
|
85
96
|
try {
|
|
86
97
|
// Build-time eval of the author's own attribute literals; a throw falls
|
|
87
98
|
// back to leaving the JSX verbatim.
|
|
88
99
|
// oxlint-disable-next-line no-new-func
|
|
89
|
-
const value = new Function(`"use strict"; return (${raw});`)(
|
|
100
|
+
const value = new Function("frontmatter", `"use strict"; return (${raw});`)(
|
|
101
|
+
frontmatter
|
|
102
|
+
);
|
|
90
103
|
return { ok: true, value };
|
|
91
104
|
} catch {
|
|
92
105
|
return { ok: false, value: undefined };
|
|
@@ -94,7 +107,10 @@ const evaluateExpression = (raw: string): { ok: boolean; value: unknown } => {
|
|
|
94
107
|
};
|
|
95
108
|
|
|
96
109
|
/** Evaluate an element's attributes into a plain props object. */
|
|
97
|
-
const readProps = (
|
|
110
|
+
const readProps = (
|
|
111
|
+
node: MdastNode,
|
|
112
|
+
frontmatter: Record<string, unknown> | undefined
|
|
113
|
+
): EvaluatedProps => {
|
|
98
114
|
const props: Record<string, unknown> = {};
|
|
99
115
|
let lossy = false;
|
|
100
116
|
for (const attribute of node.attributes ?? []) {
|
|
@@ -109,7 +125,7 @@ const readProps = (node: MdastNode): EvaluatedProps => {
|
|
|
109
125
|
} else if (typeof attribute.value === "string") {
|
|
110
126
|
props[attribute.name] = attribute.value;
|
|
111
127
|
} else {
|
|
112
|
-
const result = evaluateExpression(attribute.value.value);
|
|
128
|
+
const result = evaluateExpression(attribute.value.value, frontmatter);
|
|
113
129
|
if (result.ok) {
|
|
114
130
|
props[attribute.name] = result.value;
|
|
115
131
|
} else {
|
|
@@ -347,8 +363,9 @@ const componentHint = (registry: Record<string, ComponentMarkdown>): RegExp =>
|
|
|
347
363
|
|
|
348
364
|
const BUILT_IN_HINT = componentHint(SERIALIZERS);
|
|
349
365
|
|
|
350
|
-
/** One downlevel pass's inputs: the
|
|
366
|
+
/** One downlevel pass's inputs: the source, registry, and page metadata. */
|
|
351
367
|
interface Walk {
|
|
368
|
+
frontmatter: Record<string, unknown> | undefined;
|
|
352
369
|
registry: Record<string, ComponentMarkdown>;
|
|
353
370
|
source: string;
|
|
354
371
|
}
|
|
@@ -388,15 +405,16 @@ const serializeElement = (
|
|
|
388
405
|
node: MdastNode
|
|
389
406
|
): string | null =>
|
|
390
407
|
serializer({
|
|
391
|
-
...readProps(node),
|
|
408
|
+
...readProps(node, walk.frontmatter),
|
|
392
409
|
childComponents: (name) =>
|
|
393
410
|
(node.children ?? [])
|
|
394
411
|
.filter((child) => isJsxElement(child) && child.name === name)
|
|
395
412
|
.map((child) => ({
|
|
396
|
-
...readProps(child),
|
|
413
|
+
...readProps(child, walk.frontmatter),
|
|
397
414
|
children: renderChildren(walk, child),
|
|
398
415
|
})),
|
|
399
416
|
children: renderChildren(walk, node),
|
|
417
|
+
frontmatter: walk.frontmatter ?? {},
|
|
400
418
|
});
|
|
401
419
|
|
|
402
420
|
/**
|
|
@@ -438,10 +456,16 @@ const collectSplices = (
|
|
|
438
456
|
* `components` adds user serializers from `ai.markdownComponents`, layered
|
|
439
457
|
* over the built-ins: a same-name entry replaces the built-in serializer, and
|
|
440
458
|
* one that always returns `null` effectively opts that component out.
|
|
459
|
+
*
|
|
460
|
+
* `frontmatter` is the page's parsed front-matter data. It is put in scope
|
|
461
|
+
* when evaluating attribute expressions — so `prop={frontmatter.status}`
|
|
462
|
+
* resolves the way it does when Astro renders the page — and handed to
|
|
463
|
+
* serializers on their context.
|
|
441
464
|
*/
|
|
442
465
|
export const downlevelComponents = (
|
|
443
466
|
source: string,
|
|
444
|
-
components?: Record<string, ComponentMarkdown
|
|
467
|
+
components?: Record<string, ComponentMarkdown>,
|
|
468
|
+
frontmatter?: Record<string, unknown>
|
|
445
469
|
): string => {
|
|
446
470
|
const custom = components && Object.keys(components).length > 0;
|
|
447
471
|
const registry = custom ? { ...SERIALIZERS, ...components } : SERIALIZERS;
|
|
@@ -456,6 +480,10 @@ export const downlevelComponents = (
|
|
|
456
480
|
return source;
|
|
457
481
|
}
|
|
458
482
|
const splices: Splice[] = [];
|
|
459
|
-
collectSplices(
|
|
483
|
+
collectSplices(
|
|
484
|
+
{ frontmatter, registry, source },
|
|
485
|
+
tree.children ?? [],
|
|
486
|
+
splices
|
|
487
|
+
);
|
|
460
488
|
return splices.length > 0 ? applySplices(source, splices) : source;
|
|
461
489
|
};
|
package/src/ai/llms.ts
CHANGED
|
@@ -170,9 +170,11 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
|
|
|
170
170
|
// Resolve `<Visibility>` audiences (web-only content omitted from the
|
|
171
171
|
// agent-facing output, agents-only unwrapped), then downlevel supported
|
|
172
172
|
// components to plain Markdown.
|
|
173
|
+
const parsed = matter(raw);
|
|
173
174
|
const body = downlevelComponents(
|
|
174
|
-
applyAgentVisibility(
|
|
175
|
-
config.ai.markdownComponents
|
|
175
|
+
applyAgentVisibility(parsed.content),
|
|
176
|
+
config.ai.markdownComponents,
|
|
177
|
+
parsed.data
|
|
176
178
|
).trim();
|
|
177
179
|
const url = pageUrl(
|
|
178
180
|
page.route,
|
package/src/ai/markdown.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
+
import matter from "../core/frontmatter.ts";
|
|
3
4
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
4
5
|
import { readEntryText } from "../core/sources/read.ts";
|
|
5
6
|
import type { RouteManifestEntry } from "../core/types.ts";
|
|
@@ -47,9 +48,12 @@ export const buildRawMarkdown = async (
|
|
|
47
48
|
const entries = await Promise.all(
|
|
48
49
|
project.manifest.routes.map(async (route) => {
|
|
49
50
|
const source = applyAgentVisibility(await readRoute(route));
|
|
51
|
+
// The `.md` variant keeps the front-matter block in the output, but its
|
|
52
|
+
// data must also be in scope for `prop={frontmatter.*}` expressions.
|
|
50
53
|
const md = downlevelComponents(
|
|
51
54
|
source,
|
|
52
|
-
project.config.ai.markdownComponents
|
|
55
|
+
project.config.ai.markdownComponents,
|
|
56
|
+
matter(source).data
|
|
53
57
|
);
|
|
54
58
|
const entry: RawMarkdownEntry =
|
|
55
59
|
md === source ? { mdx: source } : { md, mdx: source };
|