blume 0.6.0 → 0.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/dist/cli/index.js +6070 -5718
- package/dist/cli/index.js.map +41 -40
- package/dist/types/core/config-input.d.ts +749 -0
- package/dist/types/core/config.d.ts +126 -3
- package/dist/types/core/schema.d.ts +10 -27
- package/dist/types/core/sources/types.d.ts +6 -0
- package/dist/types/index.d.ts +2 -1
- package/docs/advanced/changelog.mdx +10 -2
- package/docs/configuration/index.mdx +0 -2
- package/docs/content/syntax.mdx +4 -8
- package/package.json +1 -1
- package/src/astro/generate.ts +59 -24
- package/src/astro/markdown-negotiation.ts +12 -3
- package/src/astro/templates.ts +94 -12
- package/src/cli/commands/build.ts +26 -1
- package/src/cli/commands/dev.ts +30 -14
- package/src/cli/commands/doctor.ts +35 -7
- package/src/cli/commands/sync.ts +14 -2
- package/src/cli/dev-lock.ts +40 -10
- package/src/cli/env.ts +5 -1
- package/src/components/content/Update.astro +12 -2
- package/src/components/content/changelog-element.ts +62 -0
- package/src/components/islands/ask-ai.tsx +3 -1
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/layout/Header.astro +10 -2
- package/src/components/layout/NavSelector.astro +5 -3
- package/src/components/layout/PageLayout.astro +2 -1
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +47 -10
- package/src/components/layout/Search.astro +8 -3
- package/src/components/layout/nav-utils.ts +7 -3
- package/src/core/config-input.ts +923 -0
- package/src/core/config.ts +126 -3
- package/src/core/i18n.ts +6 -5
- package/src/core/links.ts +16 -1
- package/src/core/meta.ts +112 -52
- package/src/core/navigation.ts +15 -5
- package/src/core/project-graph.ts +68 -2
- package/src/core/schema.ts +8 -14
- package/src/core/sources/assets.ts +21 -5
- package/src/core/sources/cache.ts +19 -1
- package/src/core/sources/github-releases.ts +9 -3
- package/src/core/sources/mdx-remote.ts +14 -4
- package/src/core/sources/normalize.ts +13 -1
- package/src/core/sources/notion.ts +43 -7
- package/src/core/sources/resolve.ts +44 -1
- package/src/core/sources/sanity.ts +9 -3
- package/src/core/sources/types.ts +6 -0
- package/src/deploy/adapter-output.ts +82 -0
- package/src/deploy/rss.ts +3 -1
- package/src/index.ts +1 -1
- package/src/markdown/code-title.ts +11 -4
- package/src/markdown/index.ts +28 -30
- package/src/markdown/math.ts +3 -2
- package/src/markdown/package-commands.ts +13 -0
- package/src/og/card.ts +3 -1
- package/src/openapi/model.ts +2 -1
- package/src/openapi/parse.ts +9 -1
- package/src/openapi/references.ts +11 -1
- package/src/openapi/render-mdx.ts +30 -3
- package/src/openapi/source.ts +3 -1
- package/src/registry/eject.ts +21 -14
- package/src/search/documents.ts +4 -1
- package/src/theme/entry.ts +10 -3
- package/src/theme/icons.ts +7 -11
|
@@ -1,8 +1,131 @@
|
|
|
1
|
-
import type { BlumeConfig
|
|
1
|
+
import type { BlumeConfig } from "./config-input.ts";
|
|
2
|
+
import type { ResolvedConfig } from "./schema.ts";
|
|
2
3
|
import type { Diagnostic } from "./types.ts";
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* Define a Blume site's configuration with full type-checking and editor
|
|
6
|
+
* autocomplete. Place the call in `blume.config.ts` at your project root and
|
|
7
|
+
* `export default` the result:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { defineConfig } from "blume";
|
|
11
|
+
*
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* title: "Acme Docs",
|
|
14
|
+
* description: "Everything you need to build with Acme.",
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Every field is optional — an empty `defineConfig({})` produces a working
|
|
19
|
+
* site from the Markdown/MDX in your `docs/` directory. Configure only what you
|
|
20
|
+
* want to change; sensible defaults fill in the rest.
|
|
21
|
+
*
|
|
22
|
+
* This is an identity helper: it returns its input unchanged and exists purely
|
|
23
|
+
* for type inference (and as a stable home for future plugin hooks). The object
|
|
24
|
+
* is validated against the Blume schema when the CLI loads it.
|
|
25
|
+
*
|
|
26
|
+
* ## Top-level fields
|
|
27
|
+
*
|
|
28
|
+
* **Site identity**
|
|
29
|
+
* - `title` — site title, shown in the header, `<title>`, and OG images.
|
|
30
|
+
* Defaults to `"Documentation"`.
|
|
31
|
+
* - `description` — default meta description, used where a page sets none.
|
|
32
|
+
* - `logo` — brand mark. A string is an image path/URL; the object form splits
|
|
33
|
+
* an `image` mark from wordmark `text` and can override the brand `href`.
|
|
34
|
+
* - `banner` — site-wide announcement bar; a string, or `{ content, link,
|
|
35
|
+
* dismissible }`.
|
|
36
|
+
*
|
|
37
|
+
* **Content & navigation**
|
|
38
|
+
* - `content` — where content lives (`root`, defaults to `docs`) and pluggable
|
|
39
|
+
* `sources` (filesystem, remote MDX, GitHub Releases, Sanity, Notion, or a
|
|
40
|
+
* custom `ContentSource`). Omit `sources` and the top-level `root` becomes one
|
|
41
|
+
* implicit filesystem source.
|
|
42
|
+
* - `navigation` — sidebar, header `tabs`, `selectors` (version/language/product
|
|
43
|
+
* switchers), pinned `featured` links, and the `repo` link toggle. Omit
|
|
44
|
+
* `sidebar` to generate it from the content tree.
|
|
45
|
+
* - `redirects` — `{ from, to, status }` rules (301 by default).
|
|
46
|
+
* - `github` — `{ owner, repo, branch, dir }`, powering "Edit this page" links
|
|
47
|
+
* and the header repo link.
|
|
48
|
+
*
|
|
49
|
+
* **Appearance**
|
|
50
|
+
* - `theme` — `accent` color, `fonts` (curated Google Font slugs), `radius`,
|
|
51
|
+
* `mode` (`system`/`light`/`dark`), `background`, and `strict` token mode.
|
|
52
|
+
* - `markdown` — `code` (language icons, inline highlighting, line wrap),
|
|
53
|
+
* `headingAnchors`, `imageZoom`, and opt-in KaTeX `math`.
|
|
54
|
+
* - `toc` — on-page table of contents; `true`/`false` or a heading-level range.
|
|
55
|
+
* - `lastModified` — "Last updated" stamps from `git` history or frontmatter.
|
|
56
|
+
* - `feedback` — the per-page "Was this helpful?" widget (on by default).
|
|
57
|
+
* - `export` — reader-facing PDF/EPUB export actions (off by default).
|
|
58
|
+
*
|
|
59
|
+
* **Reference docs**
|
|
60
|
+
* - `openapi` — native OpenAPI reference: one real page per operation, woven
|
|
61
|
+
* into the sidebar and search. Point `sources`/`spec` at your spec.
|
|
62
|
+
* - `asyncapi` — AsyncAPI reference via the embedded Scalar renderer.
|
|
63
|
+
*
|
|
64
|
+
* **Search & AI**
|
|
65
|
+
* - `search` — search backend `provider` (`orama` by default; `pagefind`,
|
|
66
|
+
* `algolia`, `typesense`, `orama-cloud`, `mixedbread`, or `none`) plus its
|
|
67
|
+
* credential block.
|
|
68
|
+
* - `ai` — `ask` (the Ask AI chat endpoint and its provider/model) and `llmsTxt`
|
|
69
|
+
* (emit `llms.txt`).
|
|
70
|
+
* - `mcp` — expose the docs as an MCP server for connecting agents.
|
|
71
|
+
*
|
|
72
|
+
* **SEO, feeds & analytics**
|
|
73
|
+
* - `seo` — `og` images, `sitemap`, `robots`, `rss` feeds, `structuredData`
|
|
74
|
+
* JSON-LD, `agentReadability`, and robots `contentSignals`.
|
|
75
|
+
* - `analytics` — PostHog, Vercel, or arbitrary `scripts` (Plausible, Fathom,
|
|
76
|
+
* GA, …).
|
|
77
|
+
*
|
|
78
|
+
* **Deployment & i18n**
|
|
79
|
+
* - `deployment` — `site` URL (needed for absolute links, sitemaps, and OG),
|
|
80
|
+
* `adapter` (`vercel`/`node`/`netlify`/`cloudflare`), `output`
|
|
81
|
+
* (`static`/`server`), and `base` path. Auto-detected on Vercel/Netlify/
|
|
82
|
+
* Cloudflare from the platform env.
|
|
83
|
+
* - `i18n` — opt-in multi-locale: `locales`, `defaultLocale`, `parser`
|
|
84
|
+
* (`dir` vs filename `dot` suffix), and per-locale UI overrides.
|
|
85
|
+
*
|
|
86
|
+
* - `examples` — where `<Component path>` previews resolve their source from
|
|
87
|
+
* (defaults to `examples/`; supports a glob for colocated registries).
|
|
88
|
+
*
|
|
89
|
+
* @example Zero-config — just render the Markdown under `docs/`.
|
|
90
|
+
* ```ts
|
|
91
|
+
* export default defineConfig({});
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @example A production docs site with theming, search, and deployment.
|
|
95
|
+
* ```ts
|
|
96
|
+
* export default defineConfig({
|
|
97
|
+
* title: "Acme Docs",
|
|
98
|
+
* description: "Build faster with Acme.",
|
|
99
|
+
* logo: { image: "/logo.svg", text: "Acme" },
|
|
100
|
+
* github: { owner: "acme", repo: "acme" },
|
|
101
|
+
* theme: { accent: "violet", fonts: { body: "inter" }, radius: "lg" },
|
|
102
|
+
* navigation: {
|
|
103
|
+
* tabs: [
|
|
104
|
+
* { label: "Guides", path: "/guides" },
|
|
105
|
+
* { label: "API", path: "/api" },
|
|
106
|
+
* ],
|
|
107
|
+
* },
|
|
108
|
+
* search: { provider: "orama" },
|
|
109
|
+
* deployment: { site: "https://docs.acme.com", adapter: "vercel" },
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @example An OpenAPI reference with the Ask AI assistant enabled.
|
|
114
|
+
* ```ts
|
|
115
|
+
* export default defineConfig({
|
|
116
|
+
* title: "Acme API",
|
|
117
|
+
* openapi: {
|
|
118
|
+
* enabled: true,
|
|
119
|
+
* route: "/reference",
|
|
120
|
+
* sources: [{ label: "Core", spec: "./openapi.json" }],
|
|
121
|
+
* },
|
|
122
|
+
* ai: { ask: { enabled: true }, llmsTxt: true },
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @param config - The site configuration. All fields are optional.
|
|
127
|
+
* @returns The same config object, typed for inference.
|
|
128
|
+
* @see https://useblume.dev/docs for the full configuration reference.
|
|
6
129
|
*/
|
|
7
130
|
export declare const defineConfig: (config: BlumeConfig) => BlumeConfig;
|
|
8
131
|
/** Result of loading + validating a project config. */
|
|
@@ -112,9 +112,8 @@ declare const pageMetaBaseSchema: z.ZodObject<{
|
|
|
112
112
|
}>>;
|
|
113
113
|
slug: z.ZodOptional<z.ZodString>;
|
|
114
114
|
title: z.ZodOptional<z.ZodString>;
|
|
115
|
-
type: z.
|
|
115
|
+
type: z.ZodOptional<z.ZodString>;
|
|
116
116
|
}, "strict", z.ZodTypeAny, {
|
|
117
|
-
type: string;
|
|
118
117
|
search: {
|
|
119
118
|
exclude: boolean;
|
|
120
119
|
boost?: number | undefined;
|
|
@@ -138,6 +137,7 @@ declare const pageMetaBaseSchema: z.ZodObject<{
|
|
|
138
137
|
badge?: string | undefined;
|
|
139
138
|
order?: number | undefined;
|
|
140
139
|
};
|
|
140
|
+
type?: string | undefined;
|
|
141
141
|
date?: string | undefined;
|
|
142
142
|
title?: string | undefined;
|
|
143
143
|
description?: string | undefined;
|
|
@@ -317,9 +317,8 @@ export declare const pageMetaSchema: z.ZodObject<{
|
|
|
317
317
|
}>>;
|
|
318
318
|
slug: z.ZodOptional<z.ZodString>;
|
|
319
319
|
title: z.ZodOptional<z.ZodString>;
|
|
320
|
-
type: z.
|
|
320
|
+
type: z.ZodOptional<z.ZodString>;
|
|
321
321
|
}, "strict", z.ZodTypeAny, {
|
|
322
|
-
type: string;
|
|
323
322
|
search: {
|
|
324
323
|
exclude: boolean;
|
|
325
324
|
boost?: number | undefined;
|
|
@@ -343,6 +342,7 @@ export declare const pageMetaSchema: z.ZodObject<{
|
|
|
343
342
|
badge?: string | undefined;
|
|
344
343
|
order?: number | undefined;
|
|
345
344
|
};
|
|
345
|
+
type?: string | undefined;
|
|
346
346
|
date?: string | undefined;
|
|
347
347
|
title?: string | undefined;
|
|
348
348
|
description?: string | undefined;
|
|
@@ -1708,12 +1708,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1708
1708
|
* …). On by default; recognized languages only.
|
|
1709
1709
|
*/
|
|
1710
1710
|
icons: z.ZodDefault<z.ZodBoolean>;
|
|
1711
|
-
/**
|
|
1712
|
-
* Syntax-highlight inline `` `code{:lang}` `` snippets. Off by default — most
|
|
1713
|
-
* inline code (flags, file names) reads better plain; opt a snippet in with
|
|
1714
|
-
* a trailing `{:lang}` marker.
|
|
1715
|
-
*/
|
|
1716
|
-
inline: z.ZodDefault<z.ZodBoolean>;
|
|
1717
1711
|
/**
|
|
1718
1712
|
* Wrap long lines instead of scrolling horizontally. Off by default, so
|
|
1719
1713
|
* code keeps its original line breaks and overflows into a scroll area.
|
|
@@ -1721,11 +1715,9 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1721
1715
|
wrap: z.ZodDefault<z.ZodBoolean>;
|
|
1722
1716
|
}, "strict", z.ZodTypeAny, {
|
|
1723
1717
|
icons: boolean;
|
|
1724
|
-
inline: boolean;
|
|
1725
1718
|
wrap: boolean;
|
|
1726
1719
|
}, {
|
|
1727
1720
|
icons?: boolean | undefined;
|
|
1728
|
-
inline?: boolean | undefined;
|
|
1729
1721
|
wrap?: boolean | undefined;
|
|
1730
1722
|
}>>;
|
|
1731
1723
|
codeBlocks: z.ZodDefault<z.ZodObject<{
|
|
@@ -1761,15 +1753,9 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1761
1753
|
* opt a single image out with `data-no-zoom`.
|
|
1762
1754
|
*/
|
|
1763
1755
|
imageZoom: z.ZodDefault<z.ZodBoolean>;
|
|
1764
|
-
/**
|
|
1765
|
-
* Enable LaTeX math (`$…$` inline, `$$…$$` block) rendered with KaTeX.
|
|
1766
|
-
* Off by default since `$` is common in prose, shell, and code. MDX only.
|
|
1767
|
-
*/
|
|
1768
|
-
math: z.ZodDefault<z.ZodBoolean>;
|
|
1769
1756
|
}, "strict", z.ZodTypeAny, {
|
|
1770
1757
|
code: {
|
|
1771
1758
|
icons: boolean;
|
|
1772
|
-
inline: boolean;
|
|
1773
1759
|
wrap: boolean;
|
|
1774
1760
|
};
|
|
1775
1761
|
codeBlocks: {
|
|
@@ -1780,11 +1766,9 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1780
1766
|
};
|
|
1781
1767
|
headingAnchors: boolean;
|
|
1782
1768
|
imageZoom: boolean;
|
|
1783
|
-
math: boolean;
|
|
1784
1769
|
}, {
|
|
1785
1770
|
code?: {
|
|
1786
1771
|
icons?: boolean | undefined;
|
|
1787
|
-
inline?: boolean | undefined;
|
|
1788
1772
|
wrap?: boolean | undefined;
|
|
1789
1773
|
} | undefined;
|
|
1790
1774
|
codeBlocks?: {
|
|
@@ -1795,7 +1779,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1795
1779
|
} | undefined;
|
|
1796
1780
|
headingAnchors?: boolean | undefined;
|
|
1797
1781
|
imageZoom?: boolean | undefined;
|
|
1798
|
-
math?: boolean | undefined;
|
|
1799
1782
|
}>>;
|
|
1800
1783
|
mcp: z.ZodDefault<z.ZodObject<{
|
|
1801
1784
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -2664,7 +2647,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2664
2647
|
markdown: {
|
|
2665
2648
|
code: {
|
|
2666
2649
|
icons: boolean;
|
|
2667
|
-
inline: boolean;
|
|
2668
2650
|
wrap: boolean;
|
|
2669
2651
|
};
|
|
2670
2652
|
codeBlocks: {
|
|
@@ -2675,7 +2657,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2675
2657
|
};
|
|
2676
2658
|
headingAnchors: boolean;
|
|
2677
2659
|
imageZoom: boolean;
|
|
2678
|
-
math: boolean;
|
|
2679
2660
|
};
|
|
2680
2661
|
mcp: {
|
|
2681
2662
|
enabled: boolean;
|
|
@@ -3022,7 +3003,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3022
3003
|
markdown?: {
|
|
3023
3004
|
code?: {
|
|
3024
3005
|
icons?: boolean | undefined;
|
|
3025
|
-
inline?: boolean | undefined;
|
|
3026
3006
|
wrap?: boolean | undefined;
|
|
3027
3007
|
} | undefined;
|
|
3028
3008
|
codeBlocks?: {
|
|
@@ -3033,7 +3013,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3033
3013
|
} | undefined;
|
|
3034
3014
|
headingAnchors?: boolean | undefined;
|
|
3035
3015
|
imageZoom?: boolean | undefined;
|
|
3036
|
-
math?: boolean | undefined;
|
|
3037
3016
|
} | undefined;
|
|
3038
3017
|
mcp?: {
|
|
3039
3018
|
name?: string | undefined;
|
|
@@ -3102,8 +3081,12 @@ export type ResolvedConfig = z.infer<typeof blumeConfigSchema>;
|
|
|
3102
3081
|
export type ResolvedI18nConfig = z.infer<typeof i18nConfigSchema>;
|
|
3103
3082
|
/** A configured locale with display metadata. */
|
|
3104
3083
|
export type LocaleConfig = z.infer<typeof localeSchema>;
|
|
3105
|
-
/**
|
|
3106
|
-
|
|
3084
|
+
/**
|
|
3085
|
+
* User-authored config, straight off the schema. The public, hand-documented
|
|
3086
|
+
* authoring type is `BlumeConfig` in `./config-input.ts`, which a compile-time
|
|
3087
|
+
* guard keeps structurally identical to this.
|
|
3088
|
+
*/
|
|
3089
|
+
export type BlumeConfigInput = z.input<typeof blumeConfigSchema>;
|
|
3107
3090
|
/** A configured search backend. */
|
|
3108
3091
|
export type SearchProvider = (typeof searchProviders)[number];
|
|
3109
3092
|
/** Resolved robots.txt `Content-Signal` preferences (`null` when disabled). */
|
|
@@ -83,6 +83,12 @@ export interface ContentSource {
|
|
|
83
83
|
readonly staged: boolean;
|
|
84
84
|
/** Optional route prefix; the source's routes namespace under `/<prefix>/`. */
|
|
85
85
|
readonly prefix?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Resolved on-disk root, set by filesystem-backed sources only. Drives
|
|
88
|
+
* folder-meta discovery (scan under this root) and the docs-collection base;
|
|
89
|
+
* omitted by remote/CMS/staged sources that have no local tree.
|
|
90
|
+
*/
|
|
91
|
+
readonly contentRoot?: string;
|
|
86
92
|
/** Pull every entry. Called once per scan. */
|
|
87
93
|
load: () => Promise<SourceLoadResult>;
|
|
88
94
|
/** Validate the source is usable; throws a BlumeError when not. */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type { ComponentOverride, ComponentOverrides, IslandDescriptor, } from ".
|
|
|
5
5
|
export { defineMeta } from "./core/define-meta.ts";
|
|
6
6
|
export type { FolderMetaDefinition, FolderMetaFactory, } from "./core/define-meta.ts";
|
|
7
7
|
export type { UIStrings } from "./core/i18n-ui.ts";
|
|
8
|
-
export type { BlumeConfig
|
|
8
|
+
export type { BlumeConfig } from "./core/config-input.ts";
|
|
9
|
+
export type { FolderMeta, HydrationMode, ResolvedConfig, } from "./core/schema.ts";
|
|
9
10
|
export type { Diagnostic, Heading, NavNode, Navigation, NavTab, PageRecord, } from "./core/types.ts";
|
|
10
11
|
export { getBlumeVersion } from "./core/version.ts";
|
|
@@ -53,14 +53,22 @@ The optional `changelog` object adds richer metadata for the timeline and feed:
|
|
|
53
53
|
|
|
54
54
|
## The timeline page
|
|
55
55
|
|
|
56
|
-
Once you have at least one `type: changelog` entry, Blume generates a **`/changelog`** page automatically.
|
|
56
|
+
Once you have at least one `type: changelog` entry, Blume generates a **`/changelog`** page automatically. It renders as a focused, full-width timeline — no sidebar or table of contents — with each entry newest-first, showing its date, label, and `category` tag in a left rail beside its content:
|
|
57
57
|
|
|
58
|
-
- The entry **title** becomes its label — or `v{version}` when there's no title.
|
|
58
|
+
- The entry **title** becomes its label — or `v{version}` when there's no title. It links to that entry's own page, so a release is both a line in the timeline and a shareable permalink.
|
|
59
59
|
- The `category` renders as a tag next to the date.
|
|
60
60
|
- Drafts and `sidebar.hidden` entries are skipped.
|
|
61
61
|
|
|
62
62
|
The page appears only when nothing already occupies the `/changelog` route. To replace it with your own design, add a [custom page](/docs/advanced/custom-pages) at `pages/changelog.astro` — it takes over and Blume stops generating the default timeline.
|
|
63
63
|
|
|
64
|
+
### Grouped by major version
|
|
65
|
+
|
|
66
|
+
When your versions follow [semver](https://semver.org) and span more than one major, Blume paginates the timeline by major version. Only the newest major line is shown, with a **Show N.x releases** button at the bottom that reveals the next-oldest major one click at a time:
|
|
67
|
+
|
|
68
|
+
- Detection is automatic — no configuration. It kicks in only when every listed release parses as `major.minor.patch` and there is more than one major; otherwise the timeline stays flat.
|
|
69
|
+
- It tolerates the scoped tags monorepos publish, so `pkg@2.0.0` groups under `2.x` and `pkg@1.4.0` under `1.x`.
|
|
70
|
+
- It's progressive enhancement: every release is still in the page's HTML (and its RSS feed and search index), so readers without JavaScript — and crawlers — see the complete history. The button only collapses older majors once the page hydrates.
|
|
71
|
+
|
|
64
72
|
## From GitHub Releases
|
|
65
73
|
|
|
66
74
|
Rather than authoring entries by hand, point the built-in [`github-releases` source](/docs/content/sources#github-releases) at a repo and every release becomes a `type: changelog` entry — the same timeline and feed, fed straight from the releases you already publish:
|
|
@@ -49,11 +49,9 @@ export default defineConfig({
|
|
|
49
49
|
// Markdown features
|
|
50
50
|
markdown: {
|
|
51
51
|
imageZoom: true,
|
|
52
|
-
math: false,
|
|
53
52
|
code: {
|
|
54
53
|
icons: true, // language icon in the code-block header
|
|
55
54
|
wrap: false, // wrap long lines instead of scrolling
|
|
56
|
-
inline: false, // highlight inline `code{:lang}` snippets
|
|
57
55
|
},
|
|
58
56
|
},
|
|
59
57
|
|
package/docs/content/syntax.mdx
CHANGED
|
@@ -139,7 +139,7 @@ export default defineConfig({
|
|
|
139
139
|
```
|
|
140
140
|
````
|
|
141
141
|
|
|
142
|
-
Inline code can be highlighted too: add a `{:lang}` marker inside a backtick span and it's colored like a tiny code block — `useState(){:js}` or `T extends object{:ts}`.
|
|
142
|
+
Inline code can be highlighted too: add a `{:lang}` marker inside a backtick span and it's colored like a tiny code block — `useState(){:js}` or `T extends object{:ts}`. It only kicks in when you add the marker, so plain inline code stays untouched — nothing to switch on.
|
|
143
143
|
|
|
144
144
|
### Line numbers
|
|
145
145
|
|
|
@@ -372,24 +372,20 @@ The names `caution`, `error`, `important`, and `warn` are accepted as aliases fo
|
|
|
372
372
|
|
|
373
373
|
## Math
|
|
374
374
|
|
|
375
|
-
Render LaTeX with KaTeX
|
|
376
|
-
|
|
377
|
-
The Pythagorean theorem is $a^2 + b^2 = c^2$.
|
|
375
|
+
Render LaTeX with KaTeX as centered blocks — useful for math-heavy or scientific docs. Wrap a formula in `$$…$$`:
|
|
378
376
|
|
|
379
377
|
$$
|
|
380
378
|
\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}
|
|
381
379
|
$$
|
|
382
380
|
|
|
383
381
|
```md
|
|
384
|
-
The Pythagorean theorem is $a^2 + b^2 = c^2$.
|
|
385
|
-
|
|
386
382
|
$$
|
|
387
|
-
|
|
383
|
+
a^2 + b^2 = c^2
|
|
388
384
|
$$
|
|
389
385
|
```
|
|
390
386
|
|
|
391
387
|
:::note
|
|
392
|
-
Math is
|
|
388
|
+
Math is block-only and on automatically — write `$$…$$` and it renders; write none and KaTeX's stylesheet never ships. There's no inline `$…$` math: a lone `$` (currency, shell variables, code) is always left as literal text, so there's no delimiter to escape and no setting to toggle. Math is an MDX-only feature.
|
|
393
389
|
:::
|
|
394
390
|
|
|
395
391
|
## Smart punctuation
|
package/package.json
CHANGED
package/src/astro/generate.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { validateNavTargets } from "../core/nav-diagnostics.ts";
|
|
|
33
33
|
import { packageRoot } from "../core/package-root.ts";
|
|
34
34
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
35
35
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
36
|
+
import { resolveDocsCollection } from "../core/sources/resolve.ts";
|
|
36
37
|
import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
|
|
37
38
|
import type { Navigation } from "../core/types.ts";
|
|
38
39
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
@@ -330,6 +331,26 @@ export const detectNeedsReact = async (root: string): Promise<boolean> => {
|
|
|
330
331
|
return matches.length > 0;
|
|
331
332
|
};
|
|
332
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Detect whether the project authors block math (`$$…$$`) in any `.mdx`. Drives
|
|
336
|
+
* whether the generated runtime imports the `<Math>` component and KaTeX's
|
|
337
|
+
* stylesheet, so a math-free site ships no KaTeX CSS. Math parsing itself is
|
|
338
|
+
* always on but block-only, so a literal `$$` in source is a necessary
|
|
339
|
+
* condition — no false negatives. A stray `$$` (e.g. inside a code fence) merely
|
|
340
|
+
* over-includes the idempotent import, which is harmless.
|
|
341
|
+
*/
|
|
342
|
+
export const detectUsesMath = async (root: string): Promise<boolean> => {
|
|
343
|
+
const files = await glob(["**/*.mdx"], {
|
|
344
|
+
cwd: root,
|
|
345
|
+
ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
|
|
346
|
+
onlyFiles: true,
|
|
347
|
+
});
|
|
348
|
+
const contents = await Promise.all(
|
|
349
|
+
files.map((file) => readOptional(join(root, file)))
|
|
350
|
+
);
|
|
351
|
+
return contents.some((content) => content.includes("$$"));
|
|
352
|
+
};
|
|
353
|
+
|
|
333
354
|
const writeIfChanged = async (
|
|
334
355
|
path: string,
|
|
335
356
|
content: string
|
|
@@ -722,10 +743,14 @@ interface McpPlan {
|
|
|
722
743
|
|
|
723
744
|
/**
|
|
724
745
|
* Decide whether (and how) to generate the MCP server. Skipped — with a
|
|
725
|
-
* warning — when a content page
|
|
726
|
-
* keeps working.
|
|
746
|
+
* warning — when a content page or a custom `.astro` page already occupies its
|
|
747
|
+
* route, so the user's page keeps working instead of colliding.
|
|
727
748
|
*/
|
|
728
|
-
const planMcp = (
|
|
749
|
+
const planMcp = (
|
|
750
|
+
project: BlumeProject,
|
|
751
|
+
srcDir: string,
|
|
752
|
+
userPages: { pattern: string }[]
|
|
753
|
+
): McpPlan => {
|
|
729
754
|
const { config } = project;
|
|
730
755
|
const { route } = config.mcp;
|
|
731
756
|
const dir = join(srcDir, "blume-mcp");
|
|
@@ -740,11 +765,11 @@ const planMcp = (project: BlumeProject, srcDir: string): McpPlan => {
|
|
|
740
765
|
if (!config.mcp.enabled) {
|
|
741
766
|
return base;
|
|
742
767
|
}
|
|
743
|
-
if (project.graph.pages
|
|
768
|
+
if (routeIsTaken(userPages, project.graph.pages, route)) {
|
|
744
769
|
return {
|
|
745
770
|
...base,
|
|
746
771
|
warnings: [
|
|
747
|
-
`MCP server route "${route}" is already used by a content page; the MCP server was not generated. Set a different "mcp.route" in blume.config.ts.`,
|
|
772
|
+
`MCP server route "${route}" is already used by a content or custom page; the MCP server was not generated. Set a different "mcp.route" in blume.config.ts.`,
|
|
748
773
|
],
|
|
749
774
|
};
|
|
750
775
|
}
|
|
@@ -858,9 +883,13 @@ export interface GenerateResult {
|
|
|
858
883
|
* `type: changelog` entries — or when a release-backed changelog source is
|
|
859
884
|
* configured, so its route (and any nav tab pointing at it) still resolves to an
|
|
860
885
|
* empty timeline on a build where the source could not be fetched (e.g. CI
|
|
861
|
-
* without a token). Skipped when a user content page
|
|
886
|
+
* without a token). Skipped when a user content page or a custom `.astro` page
|
|
887
|
+
* already owns `/changelog`.
|
|
862
888
|
*/
|
|
863
|
-
const shouldGenerateChangelog = (
|
|
889
|
+
const shouldGenerateChangelog = (
|
|
890
|
+
project: BlumeProject,
|
|
891
|
+
userPages: { pattern: string }[]
|
|
892
|
+
): boolean => {
|
|
864
893
|
const hasChangelog = project.graph.pages.some(
|
|
865
894
|
(page) =>
|
|
866
895
|
page.contentType === "changelog" &&
|
|
@@ -869,10 +898,10 @@ const shouldGenerateChangelog = (project: BlumeProject): boolean => {
|
|
|
869
898
|
const hasChangelogSource = (project.config.content.sources ?? []).some(
|
|
870
899
|
(source) => source.type === "github-releases"
|
|
871
900
|
);
|
|
872
|
-
|
|
873
|
-
(
|
|
901
|
+
return (
|
|
902
|
+
(hasChangelog || hasChangelogSource) &&
|
|
903
|
+
!routeIsTaken(userPages, project.graph.pages, "/changelog")
|
|
874
904
|
);
|
|
875
|
-
return (hasChangelog || hasChangelogSource) && !changelogRouteTaken;
|
|
876
905
|
};
|
|
877
906
|
|
|
878
907
|
/**
|
|
@@ -933,16 +962,21 @@ export const generateRuntime = async (
|
|
|
933
962
|
const askEnabled = config.ai.ask?.enabled ?? false;
|
|
934
963
|
const exportPdf = config.export.pdf;
|
|
935
964
|
const exportEpub = config.export.epub;
|
|
936
|
-
const [
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
965
|
+
const [
|
|
966
|
+
pages,
|
|
967
|
+
detectedReact,
|
|
968
|
+
usesMath,
|
|
969
|
+
userTheme,
|
|
970
|
+
islandDiscovery,
|
|
971
|
+
exampleDiscovery,
|
|
972
|
+
] = await Promise.all([
|
|
973
|
+
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
974
|
+
detectNeedsReact(context.root),
|
|
975
|
+
detectUsesMath(context.root),
|
|
976
|
+
readOptional(context.themeFile),
|
|
977
|
+
discoverIslands(context.root),
|
|
978
|
+
discoverExamples(context.root, config.examples),
|
|
979
|
+
]);
|
|
946
980
|
// Statically analyze `components.ts` overrides (never executed): drives the
|
|
947
981
|
// `islands` group, hydration on layout/mdx overrides, string-path resolution,
|
|
948
982
|
// and the "framework component with no client mode" diagnostic.
|
|
@@ -973,7 +1007,7 @@ export const generateRuntime = async (
|
|
|
973
1007
|
// The hosted MCP server. The `.well-known` discovery docs are injected as
|
|
974
1008
|
// prerendered routes alongside user pages; the server endpoint itself is a
|
|
975
1009
|
// normal (server-rendered) page written by `writeMcpFiles`.
|
|
976
|
-
const mcp = planMcp(project, srcDir);
|
|
1010
|
+
const mcp = planMcp(project, srcDir, pages);
|
|
977
1011
|
pages.push(...mcp.discoveryPages);
|
|
978
1012
|
|
|
979
1013
|
// Staged (non-filesystem) sources materialize into `.blume/content`; keyed by
|
|
@@ -1016,6 +1050,7 @@ export const generateRuntime = async (
|
|
|
1016
1050
|
write(
|
|
1017
1051
|
join(srcDir, "content.config.ts"),
|
|
1018
1052
|
contentConfigTemplate({
|
|
1053
|
+
collection: resolveDocsCollection(config, context),
|
|
1019
1054
|
config,
|
|
1020
1055
|
context,
|
|
1021
1056
|
filesystem: hasFilesystemSource,
|
|
@@ -1028,7 +1063,7 @@ export const generateRuntime = async (
|
|
|
1028
1063
|
askEnabled,
|
|
1029
1064
|
exportEpub,
|
|
1030
1065
|
exportPdf,
|
|
1031
|
-
mathEnabled:
|
|
1066
|
+
mathEnabled: usesMath,
|
|
1032
1067
|
needsReact,
|
|
1033
1068
|
})
|
|
1034
1069
|
),
|
|
@@ -1107,7 +1142,7 @@ export const generateRuntime = async (
|
|
|
1107
1142
|
}
|
|
1108
1143
|
|
|
1109
1144
|
// Changelog index (`/changelog`), rendered through the Update timeline layout.
|
|
1110
|
-
if (shouldGenerateChangelog(project)) {
|
|
1145
|
+
if (shouldGenerateChangelog(project, pages)) {
|
|
1111
1146
|
await write(
|
|
1112
1147
|
join(srcDir, "pages", "changelog.astro"),
|
|
1113
1148
|
changelogIndexTemplate({
|
|
@@ -1201,7 +1236,7 @@ export const generateRuntime = async (
|
|
|
1201
1236
|
...pages.map((page) => page.pattern),
|
|
1202
1237
|
...referenceTabs(config).map((tab) => tab.path),
|
|
1203
1238
|
]);
|
|
1204
|
-
if (shouldGenerateChangelog(project)) {
|
|
1239
|
+
if (shouldGenerateChangelog(project, pages)) {
|
|
1205
1240
|
navTargetRoutes.add("/changelog");
|
|
1206
1241
|
}
|
|
1207
1242
|
warnings.push(
|
|
@@ -72,11 +72,20 @@ export const markdownVariantUrl = (
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const
|
|
76
|
-
|
|
75
|
+
const trimmed = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
76
|
+
// Requests arrive percent-encoded while routes are stored decoded, so a
|
|
77
|
+
// non-ASCII route (`/ja/はじめに` requested as `/ja/%E3%81%AF…`) must be
|
|
78
|
+
// decoded before the lookup. Malformed sequences stay verbatim.
|
|
79
|
+
let pathname = trimmed;
|
|
80
|
+
try {
|
|
81
|
+
pathname = decodeURIComponent(trimmed);
|
|
82
|
+
} catch {
|
|
83
|
+
// Keep the raw path; it simply won't match a content route.
|
|
84
|
+
}
|
|
77
85
|
if (!routes.has(pathname)) {
|
|
78
86
|
return null;
|
|
79
87
|
}
|
|
80
88
|
const target = pathname === "/" ? "/index" : pathname;
|
|
81
|
-
|
|
89
|
+
// Re-encode: the variant URL goes back into the request pipeline.
|
|
90
|
+
return `${prefix}${encodeURI(target)}.md${query}`;
|
|
82
91
|
};
|