blume 1.1.2 → 1.1.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 +47 -0
- package/dist/cli/index.js +284 -109
- package/dist/cli/index.js.map +33 -33
- package/dist/types/ai/component-markdown.d.ts +10 -0
- package/dist/types/core/config-input.d.ts +44 -0
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/i18n-ui.d.ts +24 -24
- package/dist/types/core/schema.d.ts +282 -114
- package/dist/types/core/types.d.ts +14 -0
- package/dist/types/openapi/references.d.ts +5 -0
- package/docs/advanced/api-reference.mdx +20 -0
- package/docs/configuration/index.mdx +27 -0
- package/package.json +1 -1
- package/src/ai/component-markdown.ts +28 -0
- package/src/ai/llms.ts +11 -2
- package/src/ai/markdown.ts +12 -6
- package/src/ai/mcp/server.ts +29 -6
- package/src/astro/examples.ts +13 -0
- package/src/astro/generate.ts +141 -58
- package/src/astro/templates.ts +65 -21
- package/src/audit/checks/duplicates.ts +15 -6
- package/src/audit/checks/indexability.ts +11 -2
- package/src/audit/checks/network.ts +22 -8
- package/src/audit/checks/sitemap.ts +42 -16
- package/src/audit/redirects.ts +12 -1
- package/src/audit/run.ts +13 -3
- package/src/audit/url.ts +21 -2
- package/src/cli/commands/audit.ts +21 -6
- package/src/cli/commands/dev.ts +19 -2
- package/src/components/content/Frame.astro +4 -1
- package/src/components/content/Prompt.astro +4 -1
- package/src/components/content/Tooltip.astro +4 -1
- package/src/components/content/Update.astro +45 -0
- package/src/components/islands/ask-ai.tsx +19 -2
- package/src/components/islands/hooks.ts +38 -11
- package/src/components/layout/Logo.astro +2 -2
- package/src/components/layout/RootLayout.astro +27 -7
- package/src/components/layout/Search.astro +5 -1
- package/src/components/layout/head-scripts.ts +22 -5
- package/src/components/openapi/ApiTagOperations.astro +17 -8
- package/src/core/config-input.ts +45 -0
- package/src/core/data.ts +2 -0
- package/src/core/date-format.ts +17 -0
- package/src/core/deployment-env.ts +7 -2
- package/src/core/graph.ts +7 -1
- package/src/core/i18n.ts +10 -2
- package/src/core/navigation.ts +7 -3
- package/src/core/project-graph.ts +9 -0
- package/src/core/schema.ts +64 -0
- package/src/core/sources/normalize.ts +69 -8
- package/src/core/sources/notion.ts +4 -2
- package/src/core/sources/sanity.ts +5 -3
- package/src/core/types.ts +16 -0
- package/src/markdown/code-title.ts +7 -1
- package/src/openapi/model.ts +31 -2
- package/src/openapi/references.ts +6 -0
- package/src/openapi/render-mdx.ts +12 -7
- package/src/openapi/scalar.ts +4 -0
- package/src/registry/eject.ts +6 -3
- package/src/theme/entry.ts +7 -0
- package/src/theme/twoslash.ts +10 -0
|
@@ -23,6 +23,20 @@ export interface Diagnostic {
|
|
|
23
23
|
suggestion?: string;
|
|
24
24
|
docsUrl?: string;
|
|
25
25
|
}
|
|
26
|
+
/** One discovered `examples/` file reduced to what Markdown downleveling needs. */
|
|
27
|
+
export interface ExampleMarkdownEntry {
|
|
28
|
+
/** Shiki language for the fenced block — the file's extension. */
|
|
29
|
+
lang: string;
|
|
30
|
+
/** Raw example source, shown verbatim in the agent-facing code fence. */
|
|
31
|
+
source: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Discovered examples keyed by their `<Component path>` (the file's location
|
|
35
|
+
* under `examples/`, sans extension). Lets the agent-facing Markdown downlevel
|
|
36
|
+
* `<Component path="…" />` to the example's source, since the live preview
|
|
37
|
+
* can't survive the trip to plain Markdown.
|
|
38
|
+
*/
|
|
39
|
+
export type ExampleLookup = Record<string, ExampleMarkdownEntry>;
|
|
26
40
|
/** A heading extracted from page content, used for the TOC and search. */
|
|
27
41
|
export interface Heading {
|
|
28
42
|
depth: number;
|
|
@@ -35,6 +35,11 @@ export interface ReferenceSource {
|
|
|
35
35
|
spec: string;
|
|
36
36
|
/** Per-block Scalar theme name override, if any (Scalar renderer only). */
|
|
37
37
|
theme?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Arbitrary Scalar config forwarded to `<ScalarComponent>` (Scalar renderer
|
|
40
|
+
* only). Takes precedence over Blume's derived spec/theme config.
|
|
41
|
+
*/
|
|
42
|
+
scalar?: Record<string, unknown>;
|
|
38
43
|
/** Display options carried through to the Blume renderer. */
|
|
39
44
|
display: ReferenceDisplay;
|
|
40
45
|
/**
|
|
@@ -104,6 +104,26 @@ openapi: {
|
|
|
104
104
|
|
|
105
105
|
A Scalar-rendered reference is a self-contained embed on its own route — it doesn't weave into Blume's sidebar, search, or `llms.txt`. Its "Try it" playground calls your **target API directly from the browser** (Blume doesn't proxy), so the API must allow cross-origin requests from the docs site (`Access-Control-Allow-Origin`). `theme` and the playground apply to the Scalar renderer only.
|
|
106
106
|
|
|
107
|
+
### Passing Scalar options
|
|
108
|
+
|
|
109
|
+
`theme` is a shorthand for the one option most people reach for, but Scalar supports many more. A `scalar` object forwards any [Scalar configuration](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) straight to the embedded reference — Blume doesn't gate the keys, so anything Scalar accepts flows through:
|
|
110
|
+
|
|
111
|
+
```ts blume.config.ts lineNumbers
|
|
112
|
+
openapi: {
|
|
113
|
+
enabled: true,
|
|
114
|
+
renderer: "scalar",
|
|
115
|
+
spec: "./openapi.yaml",
|
|
116
|
+
scalar: {
|
|
117
|
+
localization: { locale: "es" }, // translate Scalar's own UI
|
|
118
|
+
agent: { disabled: true }, // disable the Scalar Agent
|
|
119
|
+
hideTestRequestButton: true,
|
|
120
|
+
orderSchemaPropertiesBy: "preserve",
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Blume's own [`i18n`](/docs/content/i18n) translates the docs chrome, but Scalar has a separate localization system — set `scalar.localization.locale` to translate the embedded reference too. Options in the `scalar` object win over Blume's derived config, so anything set here (including `theme`, `customCss`, or the spec `content`/`url`) overrides Blume's defaults. The same `scalar` block works on the `asyncapi` reference.
|
|
126
|
+
|
|
107
127
|
## AsyncAPI
|
|
108
128
|
|
|
109
129
|
Event-driven APIs use a sibling `asyncapi` block with the same shape. AsyncAPI is rendered by Scalar (the native renderer is OpenAPI-only for now); only the default route differs (`/events`):
|
|
@@ -252,6 +252,33 @@ lastModified: 2026-06-20
|
|
|
252
252
|
|
|
253
253
|
When enabled, the date is also emitted as schema.org `dateModified` in the page's structured data.
|
|
254
254
|
|
|
255
|
+
## Date format
|
|
256
|
+
|
|
257
|
+
Both the "Last updated" stamp and the [changelog](/docs/advanced/changelog) timeline render their dates through the same `dateFormat`, so they read alike. Dates always render in the site's locale; `dateFormat` controls the _shape_. It defaults to the long form (`July 21, 2026`, `2026年7月21日`):
|
|
258
|
+
|
|
259
|
+
```ts blume.config.ts
|
|
260
|
+
dateFormat: { dateStyle: "long" },
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`dateFormat` is a pass-through to [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) options. Use a `dateStyle` preset for a length:
|
|
264
|
+
|
|
265
|
+
```ts blume.config.ts
|
|
266
|
+
dateFormat: { dateStyle: "medium" },
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Or the individual component fields for a numeric house style like `2026/07/21`:
|
|
270
|
+
|
|
271
|
+
```ts blume.config.ts
|
|
272
|
+
dateFormat: { year: "numeric", month: "2-digit", day: "2-digit" },
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
| Option | Description |
|
|
276
|
+
| --- | --- |
|
|
277
|
+
| `dateStyle` | Preset length: `"full"`, `"long"`, `"medium"`, or `"short"`. Can't be combined with the component fields. |
|
|
278
|
+
| `weekday`, `era`, `year`, `month`, `day` | Individual components, e.g. `year: "numeric"`, `month: "2-digit"`. |
|
|
279
|
+
| `timeZone` | IANA time zone. Defaults to `UTC`, so a date reads the same regardless of where the site builds. |
|
|
280
|
+
| `calendar`, `numberingSystem` | Calendar system (e.g. `"japanese"`) and numbering system (e.g. `"arab"`). |
|
|
281
|
+
|
|
255
282
|
## SEO
|
|
256
283
|
|
|
257
284
|
Open Graph images, RSS feeds, and JSON-LD structured data, grouped under `seo`. See the [SEO guide](/docs/configuration/seo) for metadata, frontmatter overrides, and the full reference.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mdxToMdast } from "satteri";
|
|
2
2
|
|
|
3
3
|
import { parseYouTubeId } from "../components/content/youtube.ts";
|
|
4
|
+
import type { ExampleLookup } from "../core/types.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Downlevel Blume's MDX components to plain Markdown for agent-facing output
|
|
@@ -336,6 +337,33 @@ const youtube: ComponentMarkdown = ({ props }) => {
|
|
|
336
337
|
return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
|
|
337
338
|
};
|
|
338
339
|
|
|
340
|
+
/** Fence `code` so its opening/closing run outlengths any backticks inside. */
|
|
341
|
+
const fencedBlock = (lang: string, code: string): string => {
|
|
342
|
+
const trimmed = code.replace(/(?<!\n)\n+$/u, "");
|
|
343
|
+
const runs = trimmed.match(/`+/gu);
|
|
344
|
+
const longest = runs ? Math.max(...runs.map((run) => run.length)) : 0;
|
|
345
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
346
|
+
return `${fence}${lang}\n${trimmed}\n${fence}`;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Build the `<Component>` serializer for a project's discovered examples. The
|
|
351
|
+
* live preview can't survive the trip to Markdown, so the agent-facing output
|
|
352
|
+
* carries the example's source — the same code the "Code" tab shows — as a
|
|
353
|
+
* fenced block. An unknown `path` (or a missing `path` prop) declines, leaving
|
|
354
|
+
* the JSX verbatim, mirroring the "no example found" note the component renders
|
|
355
|
+
* on the page.
|
|
356
|
+
*/
|
|
357
|
+
export const exampleComponentSerializers = (
|
|
358
|
+
examples: ExampleLookup
|
|
359
|
+
): Record<string, ComponentMarkdown> => ({
|
|
360
|
+
Component: ({ props }) => {
|
|
361
|
+
const path = typeof props.path === "string" ? props.path : undefined;
|
|
362
|
+
const example = path === undefined ? undefined : examples[path];
|
|
363
|
+
return example ? fencedBlock(example.lang, example.source) : null;
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
339
367
|
/**
|
|
340
368
|
* The built-in serializer registry, keyed by JSX name. `Step` and `Tab` are
|
|
341
369
|
* intentionally absent: they only carry meaning inside their containers,
|
package/src/ai/llms.ts
CHANGED
|
@@ -4,7 +4,10 @@ import type { BlumeProject } from "../core/project-graph.ts";
|
|
|
4
4
|
import { readEntryText } from "../core/sources/read.ts";
|
|
5
5
|
import type { NavNode, Navigation, PageRecord } from "../core/types.ts";
|
|
6
6
|
import { buildRssFeeds } from "../deploy/rss.ts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
downlevelComponents,
|
|
9
|
+
exampleComponentSerializers,
|
|
10
|
+
} from "./component-markdown.ts";
|
|
8
11
|
import { applyAgentVisibility } from "./visibility.ts";
|
|
9
12
|
|
|
10
13
|
// Routes carry `basePath`; a `deployment.base` subdirectory is layered on top —
|
|
@@ -163,6 +166,12 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
|
|
|
163
166
|
const pages = eligiblePages(project).toSorted((a, b) =>
|
|
164
167
|
a.route.localeCompare(b.route)
|
|
165
168
|
);
|
|
169
|
+
// Downlevel `<Component>` to its example's source; a same-name user
|
|
170
|
+
// `markdownComponents` entry is spread last and still wins.
|
|
171
|
+
const components = {
|
|
172
|
+
...exampleComponentSerializers(project.examples ?? {}),
|
|
173
|
+
...config.ai.markdownComponents,
|
|
174
|
+
};
|
|
166
175
|
|
|
167
176
|
const sections = await Promise.all(
|
|
168
177
|
pages.map(async (page) => {
|
|
@@ -173,7 +182,7 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
|
|
|
173
182
|
const parsed = matter(raw);
|
|
174
183
|
const body = downlevelComponents(
|
|
175
184
|
applyAgentVisibility(parsed.content),
|
|
176
|
-
|
|
185
|
+
components,
|
|
177
186
|
parsed.data
|
|
178
187
|
).trim();
|
|
179
188
|
const url = pageUrl(
|
package/src/ai/markdown.ts
CHANGED
|
@@ -4,7 +4,10 @@ import matter from "../core/frontmatter.ts";
|
|
|
4
4
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
5
5
|
import { readEntryText } from "../core/sources/read.ts";
|
|
6
6
|
import type { RouteManifestEntry } from "../core/types.ts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
downlevelComponents,
|
|
9
|
+
exampleComponentSerializers,
|
|
10
|
+
} from "./component-markdown.ts";
|
|
8
11
|
import { applyAgentVisibility } from "./visibility.ts";
|
|
9
12
|
|
|
10
13
|
/** One route's raw-Markdown variants. */
|
|
@@ -37,6 +40,13 @@ export const buildRawMarkdown = async (
|
|
|
37
40
|
): Promise<Record<string, RawMarkdownEntry>> => {
|
|
38
41
|
const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
|
|
39
42
|
|
|
43
|
+
// Downlevel `<Component>` to its example's source. A user `markdownComponents`
|
|
44
|
+
// entry of the same name is spread last, so it still wins.
|
|
45
|
+
const components = {
|
|
46
|
+
...exampleComponentSerializers(project.examples ?? {}),
|
|
47
|
+
...project.config.ai.markdownComponents,
|
|
48
|
+
};
|
|
49
|
+
|
|
40
50
|
const readRoute = async (route: RouteManifestEntry): Promise<string> => {
|
|
41
51
|
const page = pageById.get(route.id);
|
|
42
52
|
if (page) {
|
|
@@ -50,11 +60,7 @@ export const buildRawMarkdown = async (
|
|
|
50
60
|
const source = applyAgentVisibility(await readRoute(route));
|
|
51
61
|
// The `.md` variant keeps the front-matter block in the output, but its
|
|
52
62
|
// data must also be in scope for `prop={frontmatter.*}` expressions.
|
|
53
|
-
const md = downlevelComponents(
|
|
54
|
-
source,
|
|
55
|
-
project.config.ai.markdownComponents,
|
|
56
|
-
matter(source).data
|
|
57
|
-
);
|
|
63
|
+
const md = downlevelComponents(source, components, matter(source).data);
|
|
58
64
|
const entry: RawMarkdownEntry =
|
|
59
65
|
md === source ? { mdx: source } : { md, mdx: source };
|
|
60
66
|
return [route.path, entry] as const;
|
package/src/ai/mcp/server.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
ListToolsRequestSchema,
|
|
6
6
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
|
|
8
|
-
import { withBasePath } from "../../core/base-path.ts";
|
|
8
|
+
import { stripBasePath, withBasePath } from "../../core/base-path.ts";
|
|
9
9
|
import { buildOramaIndex, queryOramaIndex } from "../../search/orama-index.ts";
|
|
10
10
|
import type { OramaDoc } from "../../search/orama-index.ts";
|
|
11
11
|
import type { McpData } from "./data.ts";
|
|
@@ -81,12 +81,32 @@ const asLimit = (value: unknown): number => {
|
|
|
81
81
|
return Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Normalize a user-supplied route to a `pages` key (`/`, `/a/b`, no suffix).
|
|
86
|
+
* Accepts a full URL too — `search_docs` hits and llms.txt entries carry
|
|
87
|
+
* `site` + `deployment.base`, and an agent following "pass a route from
|
|
88
|
+
* `search_docs`" will hand one straight back — reducing it to its base-less,
|
|
89
|
+
* percent-decoded path.
|
|
90
|
+
*/
|
|
91
|
+
const normalizeRoute = (input: string, data: McpData): string => {
|
|
92
|
+
let value = input.trim();
|
|
93
|
+
if (/^https?:\/\//iu.test(value)) {
|
|
94
|
+
try {
|
|
95
|
+
value = new URL(value).pathname;
|
|
96
|
+
} catch {
|
|
97
|
+
// Not parseable as a URL after all; treat it as a path.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
value = decodeURI(value);
|
|
102
|
+
} catch {
|
|
103
|
+
// Malformed percent sequence — compare it as written.
|
|
104
|
+
}
|
|
105
|
+
const noTrailing = value.replace(/\/+$/u, "");
|
|
87
106
|
const noSuffix = noTrailing.replace(/\.mdx?$/u, "");
|
|
88
107
|
const withSlash = noSuffix.startsWith("/") ? noSuffix : `/${noSuffix}`;
|
|
89
|
-
|
|
108
|
+
const based = stripBasePath(data.base, withSlash);
|
|
109
|
+
return based === "" ? "/" : based;
|
|
90
110
|
};
|
|
91
111
|
|
|
92
112
|
/** Build the absolute (or root-relative) URL for a route. */
|
|
@@ -141,8 +161,11 @@ const buildServer = (
|
|
|
141
161
|
asString(args.query),
|
|
142
162
|
asLimit(args.limit)
|
|
143
163
|
);
|
|
164
|
+
// `route` is the key `get_page` takes (the tool descriptions promise
|
|
165
|
+
// it); `url` is where the page is served.
|
|
144
166
|
const results = hits.map((doc: OramaDoc) => ({
|
|
145
167
|
excerpt: excerptFor(doc),
|
|
168
|
+
route: doc.route,
|
|
146
169
|
title: doc.title,
|
|
147
170
|
url: urlFor(doc.route, data),
|
|
148
171
|
}));
|
|
@@ -150,7 +173,7 @@ const buildServer = (
|
|
|
150
173
|
}
|
|
151
174
|
|
|
152
175
|
if (name === "get_page") {
|
|
153
|
-
const key = normalizeRoute(asString(args.route));
|
|
176
|
+
const key = normalizeRoute(asString(args.route), data);
|
|
154
177
|
const markdown = data.pages[key];
|
|
155
178
|
if (markdown === undefined) {
|
|
156
179
|
return text(
|
package/src/astro/examples.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { join, relative } from "pathe";
|
|
4
4
|
import { glob } from "tinyglobby";
|
|
5
5
|
|
|
6
|
+
import type { ExampleLookup } from "../core/types.ts";
|
|
6
7
|
import type { IslandClientMode } from "./islands.ts";
|
|
7
8
|
import { readClientMode } from "./islands.ts";
|
|
8
9
|
|
|
@@ -146,3 +147,15 @@ export const discoverExamples = async (
|
|
|
146
147
|
|
|
147
148
|
return { examples, warnings };
|
|
148
149
|
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Reduce discovered examples to the `<Component path>` → source lookup the
|
|
153
|
+
* agent-facing Markdown downleveler needs (see {@link BlumeProject.examples}).
|
|
154
|
+
*/
|
|
155
|
+
export const exampleMarkdownLookup = (examples: ExampleSpec[]): ExampleLookup =>
|
|
156
|
+
Object.fromEntries(
|
|
157
|
+
examples.map((example) => [
|
|
158
|
+
example.path,
|
|
159
|
+
{ lang: example.lang, source: example.source },
|
|
160
|
+
])
|
|
161
|
+
);
|