blume 1.1.2 → 1.1.3
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 +12 -0
- package/dist/cli/index.js +96 -22
- package/dist/cli/index.js.map +15 -15
- package/dist/types/ai/component-markdown.d.ts +10 -0
- package/dist/types/core/config-input.d.ts +44 -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/astro/examples.ts +13 -0
- package/src/astro/generate.ts +49 -14
- package/src/astro/templates.ts +6 -6
- package/src/components/layout/Logo.astro +2 -2
- package/src/components/layout/RootLayout.astro +14 -5
- package/src/components/openapi/ApiTagOperations.astro +17 -8
- package/src/core/config-input.ts +45 -0
- package/src/core/date-format.ts +17 -0
- package/src/core/project-graph.ts +9 -0
- package/src/core/schema.ts +64 -0
- package/src/core/types.ts +16 -0
- package/src/openapi/references.ts +6 -0
- 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/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
|
+
);
|
package/src/astro/generate.ts
CHANGED
|
@@ -58,7 +58,7 @@ import { buildThemeCss } from "../theme/palette.ts";
|
|
|
58
58
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
59
59
|
import { planComponentSlots } from "./component-slots.ts";
|
|
60
60
|
import type { ComponentSlotPlan } from "./component-slots.ts";
|
|
61
|
-
import { discoverExamples } from "./examples.ts";
|
|
61
|
+
import { discoverExamples, exampleMarkdownLookup } from "./examples.ts";
|
|
62
62
|
import { discoverIslands } from "./islands.ts";
|
|
63
63
|
import {
|
|
64
64
|
customOgRoutes,
|
|
@@ -160,8 +160,9 @@ const resolveAstroPackageJson = (modulesDir: string): string | null => {
|
|
|
160
160
|
};
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
163
|
+
* The `astro` package reachable through the normal node_modules ancestor walk
|
|
164
|
+
* from a generated runtime — its realpath'd `package.json` plus the
|
|
165
|
+
* `node_modules` directory the walk found it in — or null when none resolves.
|
|
165
166
|
*
|
|
166
167
|
* This deliberately does not use `createRequire().resolve()`. pnpm's generated
|
|
167
168
|
* bin shim adds Blume's virtual-store dependencies to `NODE_PATH`, which
|
|
@@ -170,13 +171,21 @@ const resolveAstroPackageJson = (modulesDir: string): string | null => {
|
|
|
170
171
|
* reachable skips the dependency link and makes `import "astro/config"` fail.
|
|
171
172
|
* Walking the physical node_modules ancestors mirrors the lookup that config
|
|
172
173
|
* actually gets.
|
|
174
|
+
*
|
|
175
|
+
* The containing directory matters as much as the package: under an isolated
|
|
176
|
+
* linker the walk can find a store-deduped astro in a directory that holds
|
|
177
|
+
* nothing else of Blume's, so "the right astro resolves" does not imply "the
|
|
178
|
+
* integrations resolve" — callers must check where the hit came from.
|
|
173
179
|
*/
|
|
174
|
-
const
|
|
180
|
+
const resolvedAstroHit = (
|
|
181
|
+
fromDir: string
|
|
182
|
+
): { modulesDir: string; pkg: string } | null => {
|
|
175
183
|
let dir = normalize(fromDir);
|
|
176
184
|
while (true) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
const modulesDir = join(dir, "node_modules");
|
|
186
|
+
const pkg = resolveAstroPackageJson(modulesDir);
|
|
187
|
+
if (pkg) {
|
|
188
|
+
return { modulesDir, pkg };
|
|
180
189
|
}
|
|
181
190
|
const parent = dirname(dir);
|
|
182
191
|
if (parent === dir) {
|
|
@@ -186,6 +195,15 @@ const resolvedAstroPath = (fromDir: string): string | null => {
|
|
|
186
195
|
}
|
|
187
196
|
};
|
|
188
197
|
|
|
198
|
+
/** Whether two paths name the same physical directory (realpath equality). */
|
|
199
|
+
const sameRealDir = (a: string, b: string): boolean => {
|
|
200
|
+
try {
|
|
201
|
+
return realpathSync(a) === realpathSync(b);
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
189
207
|
/**
|
|
190
208
|
* The two places an installer can put Blume's dependencies:
|
|
191
209
|
* - `<blume>/node_modules` — deps nested under the package (workspace source,
|
|
@@ -358,7 +376,11 @@ const dropStaleDepsLink = async (
|
|
|
358
376
|
* install. An `overrides` pin plus an incremental `npm install` hoists
|
|
359
377
|
* astro to the project root (deleting Blume's nested copy) but leaves
|
|
360
378
|
* `@astrojs/mdx` and friends nested under `blume/node_modules`, where the
|
|
361
|
-
* upward walk from `.blume/` can't see them.
|
|
379
|
+
* upward walk from `.blume/` can't see them. The same shape arises under
|
|
380
|
+
* an isolated linker when the workspace itself declares astro at a version
|
|
381
|
+
* matching Blume's: the store dedupes both to one copy, so the walk finds
|
|
382
|
+
* the "correct" astro through the workspace's own direct-dep symlink — in
|
|
383
|
+
* a node_modules holding none of Blume's other deps.
|
|
362
384
|
*
|
|
363
385
|
* The repair is the same symlink: Blume's dependency directory linked in as
|
|
364
386
|
* `.blume/node_modules` so the generated config's bare specifiers (`astro`,
|
|
@@ -387,12 +409,22 @@ export const ensureDepsLink = async (
|
|
|
387
409
|
// that binds it to a superseded Blume — the probes below would otherwise
|
|
388
410
|
// pass right through it (same astro, older blume) and leave it in place.
|
|
389
411
|
await dropStaleDepsLink(join(outDir, "node_modules"), pkgDir);
|
|
390
|
-
const
|
|
412
|
+
const outDirHit = resolvedAstroHit(outDir);
|
|
391
413
|
// `.blume/` resolves the very same astro Blume's deps provide.
|
|
392
|
-
const astroCorrect = blumeAstro !== null &&
|
|
393
|
-
// Clean hoisted install: astro is correct
|
|
394
|
-
//
|
|
395
|
-
|
|
414
|
+
const astroCorrect = blumeAstro !== null && outDirHit?.pkg === blumeAstro;
|
|
415
|
+
// Clean hoisted install: astro is correct, found in Blume's own dependency
|
|
416
|
+
// directory, and the integrations sit beside it — the same walk resolves
|
|
417
|
+
// them too, so there is nothing to do. Requiring the walk to land in
|
|
418
|
+
// `astroDir` itself (not merely resolve an identical astro) matters under
|
|
419
|
+
// isolated linkers: a workspace that declares astro at a version matching
|
|
420
|
+
// Blume's gets a store-deduped symlink in its own node_modules, so the walk
|
|
421
|
+
// finds the "correct" astro in a directory holding only the workspace's
|
|
422
|
+
// direct deps — none of Blume's integrations (issue #103).
|
|
423
|
+
const walkLandsInDeps =
|
|
424
|
+
astroCorrect &&
|
|
425
|
+
outDirHit !== null &&
|
|
426
|
+
sameRealDir(outDirHit.modulesDir, astroDir);
|
|
427
|
+
if (walkLandsInDeps && mdxDir === astroDir) {
|
|
396
428
|
return null;
|
|
397
429
|
}
|
|
398
430
|
// Linking the integrations' directory yields a consistent set when it also
|
|
@@ -406,7 +438,7 @@ export const ensureDepsLink = async (
|
|
|
406
438
|
// Split layout: Blume's astro is nested (a conflicting astro took the root
|
|
407
439
|
// spot) but @astrojs/mdx hoisted away from it, binding to the shadow. Only a
|
|
408
440
|
// root pin fixes this — surface it.
|
|
409
|
-
return astroConflictWarning(blumeAstro,
|
|
441
|
+
return astroConflictWarning(blumeAstro, outDirHit?.pkg ?? null);
|
|
410
442
|
};
|
|
411
443
|
|
|
412
444
|
/**
|
|
@@ -1335,6 +1367,9 @@ export const generateRuntime = async (
|
|
|
1335
1367
|
tags: overrideTags,
|
|
1336
1368
|
warnings: overrideWarnings,
|
|
1337
1369
|
} = componentSlots;
|
|
1370
|
+
// Expose the discovered examples for agent-facing Markdown downleveling
|
|
1371
|
+
// (`<Component>` → source) before any consumer (raw `.md`, MCP, llms) runs.
|
|
1372
|
+
project.examples = exampleMarkdownLookup(exampleDiscovery.examples);
|
|
1338
1373
|
|
|
1339
1374
|
// Each island/example framework enables its Astro renderer. React also
|
|
1340
1375
|
// switches on for any project `.tsx`/`.jsx` and for Ask AI; Vue/Svelte are
|
package/src/astro/templates.ts
CHANGED
|
@@ -1445,6 +1445,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1445
1445
|
page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
|
|
1446
1446
|
headings={headings}
|
|
1447
1447
|
toc={data.config.toc}
|
|
1448
|
+
dateFormat={data.config.dateFormat}
|
|
1448
1449
|
themeMode={data.config.theme.mode}
|
|
1449
1450
|
fontCssVars={data.fontCssVars}
|
|
1450
1451
|
searchEnabled={data.config.search.enabled}
|
|
@@ -1502,6 +1503,7 @@ import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
|
1502
1503
|
import Update from "blume/components/content/Update.astro";
|
|
1503
1504
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1504
1505
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1506
|
+
import { resolveDateFormatOptions } from "blume/core/date-format.ts";
|
|
1505
1507
|
import { layoutOverrides } from "../generated/components.ts";
|
|
1506
1508
|
import data from "blume:data";
|
|
1507
1509
|
|
|
@@ -1529,8 +1531,9 @@ const localeMeta = i18n
|
|
|
1529
1531
|
const dir = localeMeta?.dir ?? "ltr";
|
|
1530
1532
|
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1531
1533
|
|
|
1532
|
-
// Formatted in the same locale as the chrome, and
|
|
1533
|
-
// per-page "last updated" stamp.
|
|
1534
|
+
// Formatted in the same locale as the chrome, and with the configured
|
|
1535
|
+
// \`dateFormat\` (UTC by default), to match the per-page "last updated" stamp.
|
|
1536
|
+
const dateFormatOptions = resolveDateFormatOptions(data.config.dateFormat);
|
|
1534
1537
|
const formatDate = (value: string | null | undefined) => {
|
|
1535
1538
|
if (!value) {
|
|
1536
1539
|
return;
|
|
@@ -1538,10 +1541,7 @@ const formatDate = (value: string | null | undefined) => {
|
|
|
1538
1541
|
const date = new Date(value);
|
|
1539
1542
|
return Number.isNaN(date.getTime())
|
|
1540
1543
|
? undefined
|
|
1541
|
-
: new Intl.DateTimeFormat(htmlLang,
|
|
1542
|
-
dateStyle: "long",
|
|
1543
|
-
timeZone: "UTC",
|
|
1544
|
-
}).format(date);
|
|
1544
|
+
: new Intl.DateTimeFormat(htmlLang, dateFormatOptions).format(date);
|
|
1545
1545
|
};
|
|
1546
1546
|
|
|
1547
1547
|
const slugify = (text: string) =>
|
|
@@ -29,7 +29,7 @@ const brandText = logo?.text ?? site.title;
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
31
|
<a
|
|
32
|
-
class="inline-flex items-center gap-2 font-semibold text-base text-foreground"
|
|
32
|
+
class="inline-flex min-w-0 items-center gap-2 font-semibold text-base text-foreground"
|
|
33
33
|
href={withBase(brandHref)}
|
|
34
34
|
>
|
|
35
35
|
{
|
|
@@ -71,5 +71,5 @@ const brandText = logo?.text ?? site.title;
|
|
|
71
71
|
</>
|
|
72
72
|
))
|
|
73
73
|
}
|
|
74
|
-
{brandText && <span>{brandText}</span>}
|
|
74
|
+
{brandText && <span class="truncate">{brandText}</span>}
|
|
75
75
|
</a>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
3
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
4
|
+
import { resolveDateFormatOptions } from "../../core/date-format.ts";
|
|
5
|
+
import type { ResolvedDateFormat } from "../../core/schema.ts";
|
|
4
6
|
import type { BlumeClientData } from "../../core/data.ts";
|
|
5
7
|
import type {
|
|
6
8
|
Heading,
|
|
@@ -151,6 +153,11 @@ interface Props {
|
|
|
151
153
|
clientData?: BlumeClientData | null;
|
|
152
154
|
/** Table-of-contents settings (`toc` config): visibility + heading range. */
|
|
153
155
|
toc?: { enabled: boolean; maxLevel: number; minLevel: number };
|
|
156
|
+
/**
|
|
157
|
+
* Date-formatting options (`dateFormat` config) for the "last updated" stamp,
|
|
158
|
+
* shared with the changelog timeline. Defaults to the long form when omitted.
|
|
159
|
+
*/
|
|
160
|
+
dateFormat?: ResolvedDateFormat;
|
|
154
161
|
/**
|
|
155
162
|
* Content-column preset. `"bare"` (the generated changelog index) drops both
|
|
156
163
|
* the sidebar and the table of contents and centers a single wide column;
|
|
@@ -202,6 +209,7 @@ const {
|
|
|
202
209
|
layout = {},
|
|
203
210
|
clientData,
|
|
204
211
|
toc = { enabled: true, maxLevel: 3, minLevel: 2 },
|
|
212
|
+
dateFormat,
|
|
205
213
|
contentLayout = "default",
|
|
206
214
|
} = Astro.props;
|
|
207
215
|
|
|
@@ -288,14 +296,15 @@ const twitterCard = ogImage ? "summary_large_image" : "summary";
|
|
|
288
296
|
const xSite = normalizeXHandle(x?.handle);
|
|
289
297
|
const xCreator = normalizeXHandle(x?.creator);
|
|
290
298
|
|
|
291
|
-
// "Last updated on <date>" —
|
|
299
|
+
// "Last updated on <date>" — the configured `dateFormat`, in UTC (unless the
|
|
300
|
+
// config names a zone) so it matches the changelog timeline.
|
|
292
301
|
const lastModifiedDate = lastModified ? new Date(lastModified) : null;
|
|
293
302
|
const formattedLastModified =
|
|
294
303
|
lastModifiedDate && !Number.isNaN(lastModifiedDate.getTime())
|
|
295
|
-
? new Intl.DateTimeFormat(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
? new Intl.DateTimeFormat(
|
|
305
|
+
locale || "en",
|
|
306
|
+
resolveDateFormatOptions(dateFormat)
|
|
307
|
+
).format(lastModifiedDate)
|
|
299
308
|
: null;
|
|
300
309
|
|
|
301
310
|
// The hosted MCP server's absolute URL, used by the page-actions install menu.
|
|
@@ -25,16 +25,25 @@ const operations = Object.values(specs[source]?.operations ?? {}).filter(
|
|
|
25
25
|
{operations.map((operation) => (
|
|
26
26
|
<li>
|
|
27
27
|
<a
|
|
28
|
-
class="flex items-
|
|
28
|
+
class="flex items-start gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
|
|
29
29
|
href={withBase(operation.route)}
|
|
30
30
|
>
|
|
31
|
-
<MethodBadge method={operation.method} />
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
<MethodBadge class="mt-0.5 shrink-0" method={operation.method} />
|
|
32
|
+
{/* Title over path, stacked, so a long summary and a long route each
|
|
33
|
+
get the full row width instead of being squeezed side by side. */}
|
|
34
|
+
<div class="flex flex-col gap-0.5">
|
|
35
|
+
<span class="break-words font-medium text-foreground text-sm">
|
|
36
|
+
{operation.summary || operation.path}
|
|
37
|
+
</span>
|
|
38
|
+
{/* The path doubles as the label when the spec sets no summary, so
|
|
39
|
+
only repeat it as the reference line when it adds information;
|
|
40
|
+
break-all keeps a long route wrapping inside the card. */}
|
|
41
|
+
{operation.summary && (
|
|
42
|
+
<code class="break-all text-muted-foreground text-xs">
|
|
43
|
+
{operation.path}
|
|
44
|
+
</code>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
38
47
|
</a>
|
|
39
48
|
</li>
|
|
40
49
|
))}
|
package/src/core/config-input.ts
CHANGED
|
@@ -897,6 +897,13 @@ export interface OpenApiConfig {
|
|
|
897
897
|
renderer?: "blume" | "scalar";
|
|
898
898
|
/** Where the reference mounts. Defaults to `/reference`. */
|
|
899
899
|
route?: string;
|
|
900
|
+
/**
|
|
901
|
+
* Extra Scalar options forwarded verbatim to the embedded `<ScalarComponent>`
|
|
902
|
+
* (Scalar renderer only) — e.g. `localization`, `agent`,
|
|
903
|
+
* `hideTestRequestButton`, `orderSchemaPropertiesBy`. These win over Blume's
|
|
904
|
+
* derived spec/theme config, so it's a full escape hatch to Scalar's API.
|
|
905
|
+
*/
|
|
906
|
+
scalar?: Record<string, unknown>;
|
|
900
907
|
/** One or more specs; each renders on its own route by default. */
|
|
901
908
|
sources?: OpenApiSource[];
|
|
902
909
|
/** Shorthand for a single source: `sources: [{ spec }]`. */
|
|
@@ -915,6 +922,12 @@ export interface AsyncApiConfig {
|
|
|
915
922
|
enabled?: boolean;
|
|
916
923
|
/** Where the reference mounts. Defaults to `/events`. */
|
|
917
924
|
route?: string;
|
|
925
|
+
/**
|
|
926
|
+
* Extra Scalar options forwarded verbatim to the embedded `<ScalarComponent>`.
|
|
927
|
+
* These win over Blume's derived spec/theme config — a full escape hatch to
|
|
928
|
+
* Scalar's API.
|
|
929
|
+
*/
|
|
930
|
+
scalar?: Record<string, unknown>;
|
|
918
931
|
/** One or more specs. */
|
|
919
932
|
sources?: OpenApiSource[];
|
|
920
933
|
/** Shorthand for a single source. */
|
|
@@ -1003,6 +1016,33 @@ export type LastModifiedConfig =
|
|
|
1003
1016
|
type?: "git" | "frontmatter";
|
|
1004
1017
|
};
|
|
1005
1018
|
|
|
1019
|
+
/**
|
|
1020
|
+
* Date presentation for the "last updated" stamp and the changelog timeline —
|
|
1021
|
+
* a curated pass-through to `Intl.DateTimeFormat`, shared by both surfaces.
|
|
1022
|
+
* Defaults to `{ dateStyle: "long" }`. Dates render in UTC unless `timeZone` is
|
|
1023
|
+
* set. `dateStyle` is a preset and can't be combined with the component fields.
|
|
1024
|
+
*/
|
|
1025
|
+
export interface DateFormatConfig {
|
|
1026
|
+
/** Preset date length; mutually exclusive with the component fields below. */
|
|
1027
|
+
dateStyle?: "full" | "long" | "medium" | "short";
|
|
1028
|
+
/** Weekday representation. */
|
|
1029
|
+
weekday?: "long" | "short" | "narrow";
|
|
1030
|
+
/** Era representation (e.g. the Japanese imperial era). */
|
|
1031
|
+
era?: "long" | "short" | "narrow";
|
|
1032
|
+
/** Year representation. */
|
|
1033
|
+
year?: "numeric" | "2-digit";
|
|
1034
|
+
/** Month representation. */
|
|
1035
|
+
month?: "numeric" | "2-digit" | "long" | "short" | "narrow";
|
|
1036
|
+
/** Day representation. */
|
|
1037
|
+
day?: "numeric" | "2-digit";
|
|
1038
|
+
/** IANA time zone (e.g. `Asia/Tokyo`). Defaults to `UTC`. */
|
|
1039
|
+
timeZone?: string;
|
|
1040
|
+
/** Calendar system (e.g. `japanese`, `buddhist`). */
|
|
1041
|
+
calendar?: string;
|
|
1042
|
+
/** Numbering system (e.g. `latn`, `arab`). */
|
|
1043
|
+
numberingSystem?: string;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1006
1046
|
/**
|
|
1007
1047
|
* On-page table of contents. `true`/`false` toggles it; the object form narrows
|
|
1008
1048
|
* the heading range. Defaults to on, H2–H3.
|
|
@@ -1046,6 +1086,11 @@ export interface BlumeConfig {
|
|
|
1046
1086
|
basePath?: string;
|
|
1047
1087
|
/** Where content lives and how it's discovered. */
|
|
1048
1088
|
content?: ContentConfig;
|
|
1089
|
+
/**
|
|
1090
|
+
* Date presentation for the "last updated" stamp and the changelog timeline.
|
|
1091
|
+
* Pass-through `Intl.DateTimeFormat` options; defaults to `{ dateStyle: "long" }`.
|
|
1092
|
+
*/
|
|
1093
|
+
dateFormat?: DateFormatConfig;
|
|
1049
1094
|
/** Where and how the site deploys (site URL, adapter, output mode). */
|
|
1050
1095
|
deployment?: DeploymentConfig;
|
|
1051
1096
|
/** Default meta description, used where a page sets none. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ResolvedDateFormat } from "./schema.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The default date presentation — the long form (`July 21, 2026`,
|
|
5
|
+
* `2026年7月21日`) both stamps used before `dateFormat` was configurable.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_DATE_FORMAT: ResolvedDateFormat = { dateStyle: "long" };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a configured `dateFormat` into `Intl.DateTimeFormat` options for the
|
|
11
|
+
* per-page "last updated" stamp and the changelog timeline. Both surfaces call
|
|
12
|
+
* this so they format alike. Dates render in UTC unless the config names a
|
|
13
|
+
* `timeZone`, so a stamp reads the same regardless of the build machine's zone.
|
|
14
|
+
*/
|
|
15
|
+
export const resolveDateFormatOptions = (
|
|
16
|
+
format: ResolvedDateFormat = DEFAULT_DATE_FORMAT
|
|
17
|
+
): Intl.DateTimeFormatOptions => ({ timeZone: "UTC", ...format });
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
BlumeManifest,
|
|
21
21
|
ContentGraph,
|
|
22
22
|
Diagnostic,
|
|
23
|
+
ExampleLookup,
|
|
23
24
|
PageRecord,
|
|
24
25
|
ProjectContext,
|
|
25
26
|
} from "./types.ts";
|
|
@@ -74,6 +75,14 @@ export interface BlumeProject {
|
|
|
74
75
|
droppedPages: number;
|
|
75
76
|
/** The instantiated content sources, for lazy entry reads (search/AI/raw). */
|
|
76
77
|
sources: ContentSource[];
|
|
78
|
+
/**
|
|
79
|
+
* Discovered `examples/` sources keyed by `<Component path>`, attached by the
|
|
80
|
+
* runtime/eject layer after {@link scanProject} (example discovery is an Astro
|
|
81
|
+
* concern, so core doesn't run it). Undefined until then; the agent-facing
|
|
82
|
+
* Markdown downleveler reads it to turn `<Component path="…" />` into the
|
|
83
|
+
* example's source. Empty when the project has no examples.
|
|
84
|
+
*/
|
|
85
|
+
examples?: ExampleLookup;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
/**
|