blume 1.4.3 → 1.5.1
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 +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +1784 -633
- package/dist/cli/index.js.map +111 -106
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +80 -28
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +19 -3
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/configuration/theming.mdx +4 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +2 -1
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +64 -34
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +181 -40
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageLayout.astro +72 -3
- package/src/components/layout/ReferenceLayout.astro +2 -1
- package/src/components/layout/RootLayout.astro +20 -1
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +89 -28
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +19 -3
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/last-modified.ts +49 -0
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +26 -3
- package/src/core/schema.ts +214 -68
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +45 -18
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +33 -12
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/entry.ts +24 -2
- package/src/theme/fonts.ts +83 -7
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
|
@@ -87,8 +87,8 @@ const headerValues = (
|
|
|
87
87
|
params: ParamLike[],
|
|
88
88
|
schemas: Record<string, SchemaLike>,
|
|
89
89
|
auth: SampleAuth | undefined
|
|
90
|
-
)
|
|
91
|
-
const headers
|
|
90
|
+
) => {
|
|
91
|
+
const headers = { ...auth?.headers };
|
|
92
92
|
for (const param of params) {
|
|
93
93
|
if (param.in === "header" && param.required && param.name) {
|
|
94
94
|
headers[param.name] = String(
|
|
@@ -230,14 +230,14 @@ const LANGUAGES: SampleLanguage[] = [
|
|
|
230
230
|
{ build: pythonSnippet, id: "python", label: "Python", lang: "python" },
|
|
231
231
|
];
|
|
232
232
|
|
|
233
|
-
const ALIASES
|
|
234
|
-
bash
|
|
235
|
-
javascript
|
|
236
|
-
node
|
|
237
|
-
py
|
|
238
|
-
shell
|
|
239
|
-
typescript
|
|
240
|
-
|
|
233
|
+
const ALIASES = new Map([
|
|
234
|
+
["bash", "curl"],
|
|
235
|
+
["javascript", "js"],
|
|
236
|
+
["node", "js"],
|
|
237
|
+
["py", "python"],
|
|
238
|
+
["shell", "curl"],
|
|
239
|
+
["typescript", "js"],
|
|
240
|
+
]);
|
|
241
241
|
|
|
242
242
|
/** The sample languages to render, resolved from config ids (unknown ids dropped). */
|
|
243
243
|
export const sampleLanguages = (ids: string[]): SampleLanguage[] => {
|
|
@@ -246,7 +246,7 @@ export const sampleLanguages = (ids: string[]): SampleLanguage[] => {
|
|
|
246
246
|
const out: SampleLanguage[] = [];
|
|
247
247
|
const seen = new Set<string>();
|
|
248
248
|
for (const raw of wanted) {
|
|
249
|
-
const id = ALIASES
|
|
249
|
+
const id = ALIASES.get(raw.toLowerCase()) ?? raw.toLowerCase();
|
|
250
250
|
const language = byId.get(id);
|
|
251
251
|
if (language && !seen.has(id)) {
|
|
252
252
|
seen.add(id);
|
|
@@ -65,18 +65,20 @@ const GROUPS = ["mdx", "layout", "islands"] as const;
|
|
|
65
65
|
const GROUP_SET = new Set<string>(GROUPS);
|
|
66
66
|
type Group = (typeof GROUPS)[number];
|
|
67
67
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
const isGroup = (name: string): name is Group => GROUP_SET.has(name);
|
|
69
|
+
|
|
70
|
+
const FRAMEWORK_BY_EXT = new Map<string, OverrideFramework>([
|
|
71
|
+
["jsx", "react"],
|
|
72
|
+
["svelte", "svelte"],
|
|
73
|
+
["tsx", "react"],
|
|
74
|
+
["vue", "vue"],
|
|
75
|
+
]);
|
|
74
76
|
|
|
75
|
-
const FRAMEWORK_LABEL
|
|
77
|
+
const FRAMEWORK_LABEL = {
|
|
76
78
|
react: "React",
|
|
77
79
|
svelte: "Svelte",
|
|
78
80
|
vue: "Vue",
|
|
79
|
-
}
|
|
81
|
+
} satisfies Record<OverrideFramework, string>;
|
|
80
82
|
|
|
81
83
|
/** Extensions probed (in order) when a specifier omits one. */
|
|
82
84
|
const COMPONENT_EXTS = [
|
|
@@ -90,7 +92,7 @@ const COMPONENT_EXTS = [
|
|
|
90
92
|
"svelte",
|
|
91
93
|
];
|
|
92
94
|
|
|
93
|
-
const HYDRATION_MODES = new Set<HydrationMode>([
|
|
95
|
+
const HYDRATION_MODES: ReadonlySet<string> = new Set<HydrationMode>([
|
|
94
96
|
"idle",
|
|
95
97
|
"load",
|
|
96
98
|
"media",
|
|
@@ -98,6 +100,9 @@ const HYDRATION_MODES = new Set<HydrationMode>([
|
|
|
98
100
|
"visible",
|
|
99
101
|
]);
|
|
100
102
|
|
|
103
|
+
const isHydrationMode = (value: string): value is HydrationMode =>
|
|
104
|
+
HYDRATION_MODES.has(value);
|
|
105
|
+
|
|
101
106
|
interface ImportBinding {
|
|
102
107
|
/** Exported name: `"default"` or a named export. */
|
|
103
108
|
imported: string;
|
|
@@ -226,7 +231,7 @@ const toImport = (
|
|
|
226
231
|
}
|
|
227
232
|
}
|
|
228
233
|
return {
|
|
229
|
-
framework: FRAMEWORK_BY_EXT
|
|
234
|
+
framework: FRAMEWORK_BY_EXT.get(extension) ?? null,
|
|
230
235
|
name: imported,
|
|
231
236
|
path,
|
|
232
237
|
};
|
|
@@ -270,9 +275,9 @@ const applyDescriptorProperty = (
|
|
|
270
275
|
} else if (
|
|
271
276
|
name === "client" &&
|
|
272
277
|
ts.isStringLiteral(init) &&
|
|
273
|
-
|
|
278
|
+
isHydrationMode(init.text)
|
|
274
279
|
) {
|
|
275
|
-
descriptor.client = init.text
|
|
280
|
+
descriptor.client = init.text;
|
|
276
281
|
} else if (name === "media" && ts.isStringLiteral(init)) {
|
|
277
282
|
descriptor.media = init.text;
|
|
278
283
|
}
|
|
@@ -334,13 +339,14 @@ const finalize = (
|
|
|
334
339
|
);
|
|
335
340
|
}
|
|
336
341
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
342
|
+
const normalized: NormalizedOverride = { identifier, key, source };
|
|
343
|
+
if (client) {
|
|
344
|
+
normalized.client = client;
|
|
345
|
+
}
|
|
346
|
+
if (media) {
|
|
347
|
+
normalized.media = media;
|
|
348
|
+
}
|
|
349
|
+
return normalized;
|
|
344
350
|
};
|
|
345
351
|
|
|
346
352
|
const normalizeEntry = (
|
|
@@ -446,22 +452,21 @@ const collectGroupOverrides = (
|
|
|
446
452
|
}
|
|
447
453
|
const name = propName(property.name);
|
|
448
454
|
if (
|
|
449
|
-
!(name &&
|
|
455
|
+
!(name && isGroup(name)) ||
|
|
450
456
|
!ts.isObjectLiteralExpression(property.initializer)
|
|
451
457
|
) {
|
|
452
458
|
return;
|
|
453
459
|
}
|
|
454
|
-
const group = name as Group;
|
|
455
460
|
for (const entry of property.initializer.properties) {
|
|
456
461
|
const normalized = normalizeEntry(
|
|
457
462
|
entry,
|
|
458
|
-
|
|
463
|
+
name,
|
|
459
464
|
imports,
|
|
460
465
|
dir,
|
|
461
466
|
result.warnings
|
|
462
467
|
);
|
|
463
468
|
if (normalized) {
|
|
464
|
-
result[
|
|
469
|
+
result[name].push(normalized);
|
|
465
470
|
}
|
|
466
471
|
}
|
|
467
472
|
};
|
package/src/core/config-input.ts
CHANGED
|
@@ -510,7 +510,7 @@ export type FontInput =
|
|
|
510
510
|
export interface FontsConfig {
|
|
511
511
|
/** Body / prose font. Defaults to `inter`. */
|
|
512
512
|
body?: FontInput;
|
|
513
|
-
/** Display / heading font. Defaults to `inter
|
|
513
|
+
/** Display / heading font. Defaults to `inter` (shared with the body). */
|
|
514
514
|
display?: FontInput;
|
|
515
515
|
/** Monospace / code font. Defaults to `ibm-plex-mono`. */
|
|
516
516
|
mono?: FontInput;
|
|
@@ -761,6 +761,7 @@ export interface AiConfig {
|
|
|
761
761
|
/** Web Bot Auth signature directory. Off until at least one key is listed. */
|
|
762
762
|
export interface WebBotAuthConfig {
|
|
763
763
|
/** Public JWKs to publish (e.g. an Ed25519 key: `kty: "OKP"`, `crv: "Ed25519"`, `x: …`). */
|
|
764
|
+
// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- mirrors the schema's `z.record(z.unknown())` (the drift guard requires it); JWK parameters are validated at parse time, not typed.
|
|
764
765
|
keys?: Record<string, unknown>[];
|
|
765
766
|
}
|
|
766
767
|
|
|
@@ -838,6 +839,59 @@ export interface I18nConfig {
|
|
|
838
839
|
ui?: Record<string, Record<string, Record<string, string>>>;
|
|
839
840
|
}
|
|
840
841
|
|
|
842
|
+
// ---------------------------------------------------------------------------
|
|
843
|
+
// Versions
|
|
844
|
+
// ---------------------------------------------------------------------------
|
|
845
|
+
|
|
846
|
+
/** A frozen documentation snapshot: a directory under the content root. */
|
|
847
|
+
export interface ArchivedVersionInput {
|
|
848
|
+
/**
|
|
849
|
+
* The "you're viewing an old version" notice: `true` (default) for the
|
|
850
|
+
* built-in message, a string for custom copy, `false` to hide it.
|
|
851
|
+
*/
|
|
852
|
+
banner?: boolean | string;
|
|
853
|
+
/**
|
|
854
|
+
* Where this version's pages point their canonical URL. `latest` (default)
|
|
855
|
+
* targets the same page in the current docs when it still exists (self
|
|
856
|
+
* otherwise); `self` keeps every page authoritative.
|
|
857
|
+
*/
|
|
858
|
+
canonical?: "latest" | "self";
|
|
859
|
+
/**
|
|
860
|
+
* Directory name under the content root, and the URL segment. Must start
|
|
861
|
+
* with a letter (e.g. `v1.0`).
|
|
862
|
+
*/
|
|
863
|
+
id: string;
|
|
864
|
+
/** Switcher label; defaults to the id. */
|
|
865
|
+
label?: string;
|
|
866
|
+
/** Emit `noindex` on every page of this version. Defaults to `false`. */
|
|
867
|
+
noindex?: boolean;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Docs versioning. Opt-in: the latest docs live at the content root with
|
|
872
|
+
* unprefixed URLs, and each archived version is a frozen snapshot directory
|
|
873
|
+
* (`content/docs/<id>/`) cut with `blume version <id>`. Archived means frozen:
|
|
874
|
+
* snapshots carry their own translations and are never retranslated.
|
|
875
|
+
*/
|
|
876
|
+
export interface VersionsConfig {
|
|
877
|
+
/** Frozen snapshots, newest first — this order is the switcher order. */
|
|
878
|
+
archived?: ArchivedVersionInput[];
|
|
879
|
+
/** Labels the unprefixed tree (the latest docs) in the switcher. */
|
|
880
|
+
current: {
|
|
881
|
+
/** Small tag rendered next to the label (e.g. `Latest`). */
|
|
882
|
+
badge?: string;
|
|
883
|
+
label: string;
|
|
884
|
+
};
|
|
885
|
+
switcher?: {
|
|
886
|
+
/**
|
|
887
|
+
* Where switching lands when the page has no equivalent in the target
|
|
888
|
+
* version: `same-page` (default) goes to the equivalent when it exists
|
|
889
|
+
* (version root otherwise); `root` always goes to the version root.
|
|
890
|
+
*/
|
|
891
|
+
redirect?: "same-page" | "root";
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
841
895
|
// ---------------------------------------------------------------------------
|
|
842
896
|
// Deployment & redirects
|
|
843
897
|
// ---------------------------------------------------------------------------
|
|
@@ -1112,28 +1166,24 @@ export interface ReactConfig {
|
|
|
1112
1166
|
// ---------------------------------------------------------------------------
|
|
1113
1167
|
|
|
1114
1168
|
/**
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
* SPA (a single self-contained route).
|
|
1169
|
+
* The shared shape of both API-reference blocks (`openapi`, `asyncapi`). Only
|
|
1170
|
+
* the per-block defaults differ; those are documented on the extending
|
|
1171
|
+
* interfaces.
|
|
1119
1172
|
*/
|
|
1120
|
-
|
|
1121
|
-
/** Code-sample languages shown per operation (Blume renderer). */
|
|
1122
|
-
codeSamples?: string[];
|
|
1173
|
+
interface ReferenceConfig {
|
|
1123
1174
|
/** Turn the reference on. Defaults to `false`. */
|
|
1124
1175
|
enabled?: boolean;
|
|
1125
1176
|
/** Start nested schema rows expanded (Blume renderer). Defaults to `false`. */
|
|
1126
1177
|
expandSchemas?: boolean;
|
|
1127
1178
|
/** Who renders the reference. Defaults to `blume`. */
|
|
1128
1179
|
renderer?: "blume" | "scalar";
|
|
1129
|
-
/** Where the reference mounts. Defaults to `/reference`. */
|
|
1130
|
-
route?: string;
|
|
1131
1180
|
/**
|
|
1132
1181
|
* Extra Scalar options forwarded verbatim to the embedded `<ScalarComponent>`
|
|
1133
1182
|
* (Scalar renderer only) — e.g. `localization`, `agent`,
|
|
1134
1183
|
* `hideTestRequestButton`, `orderSchemaPropertiesBy`. These win over Blume's
|
|
1135
1184
|
* derived spec/theme config, so it's a full escape hatch to Scalar's API.
|
|
1136
1185
|
*/
|
|
1186
|
+
// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- mirrors the schema's `z.record(z.unknown())` (the drift guard requires it); the values are Scalar's own API surface, deliberately unmodeled.
|
|
1137
1187
|
scalar?: Record<string, unknown>;
|
|
1138
1188
|
/** One or more specs; each renders on its own route by default. */
|
|
1139
1189
|
sources?: OpenApiSource[];
|
|
@@ -1144,27 +1194,36 @@ export interface OpenApiConfig {
|
|
|
1144
1194
|
}
|
|
1145
1195
|
|
|
1146
1196
|
/**
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
* `
|
|
1197
|
+
* OpenAPI reference. By default (`renderer: "blume"`) Blume renders its own UI:
|
|
1198
|
+
* one real page per operation, grouped by tag in the sidebar and included in
|
|
1199
|
+
* search, llms.txt, and OG. Set `renderer: "scalar"` for the embedded Scalar
|
|
1200
|
+
* SPA (a single self-contained route).
|
|
1150
1201
|
*/
|
|
1151
|
-
export interface
|
|
1152
|
-
/**
|
|
1153
|
-
|
|
1154
|
-
|
|
1202
|
+
export interface OpenApiConfig extends ReferenceConfig {
|
|
1203
|
+
/**
|
|
1204
|
+
* Code-sample languages shown per operation (Blume renderer). Defaults to
|
|
1205
|
+
* `["curl", "js", "python"]`.
|
|
1206
|
+
*/
|
|
1207
|
+
codeSamples?: string[];
|
|
1208
|
+
/** Where the reference mounts. Defaults to `/reference`. */
|
|
1155
1209
|
route?: string;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* AsyncAPI reference. Same shape as {@link OpenApiConfig}: by default
|
|
1214
|
+
* (`renderer: "blume"`) Blume normalizes the spec to AsyncAPI 3.x and renders
|
|
1215
|
+
* its own UI — one real page per operation, grouped by tag (or channel) in the
|
|
1216
|
+
* sidebar and included in search, llms.txt, and OG. Set `renderer: "scalar"`
|
|
1217
|
+
* for the embedded Scalar SPA (a single self-contained route).
|
|
1218
|
+
*/
|
|
1219
|
+
export interface AsyncApiConfig extends ReferenceConfig {
|
|
1156
1220
|
/**
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
* Scalar's API.
|
|
1221
|
+
* Code-sample tools shown per operation (Blume renderer). Defaults to every
|
|
1222
|
+
* tool appropriate to the operation's protocol binding.
|
|
1160
1223
|
*/
|
|
1161
|
-
|
|
1162
|
-
/**
|
|
1163
|
-
|
|
1164
|
-
/** Shorthand for a single source. */
|
|
1165
|
-
spec?: string;
|
|
1166
|
-
/** Scalar theme name. */
|
|
1167
|
-
theme?: string;
|
|
1224
|
+
codeSamples?: string[];
|
|
1225
|
+
/** Where the reference mounts. Defaults to `/events`. */
|
|
1226
|
+
route?: string;
|
|
1168
1227
|
}
|
|
1169
1228
|
|
|
1170
1229
|
// ---------------------------------------------------------------------------
|
|
@@ -1302,7 +1361,7 @@ export interface BlumeConfig {
|
|
|
1302
1361
|
ai?: AiConfig;
|
|
1303
1362
|
/** Analytics providers (PostHog, Vercel, or arbitrary scripts). */
|
|
1304
1363
|
analytics?: AnalyticsConfig;
|
|
1305
|
-
/** AsyncAPI reference (
|
|
1364
|
+
/** AsyncAPI reference (native renderer by default, Scalar opt-out). */
|
|
1306
1365
|
asyncapi?: AsyncApiConfig;
|
|
1307
1366
|
/** Site-wide announcement banner shown above the header. */
|
|
1308
1367
|
banner?: BannerConfig;
|
|
@@ -1375,6 +1434,8 @@ export interface BlumeConfig {
|
|
|
1375
1434
|
title?: string;
|
|
1376
1435
|
/** On-page table of contents. Defaults to on (H2–H3). */
|
|
1377
1436
|
toc?: TocConfig;
|
|
1437
|
+
/** Docs versioning (opt-in frozen snapshots with a version switcher). */
|
|
1438
|
+
versions?: VersionsConfig;
|
|
1378
1439
|
}
|
|
1379
1440
|
|
|
1380
1441
|
// ---------------------------------------------------------------------------
|
package/src/core/config.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
3
5
|
import type { BlumeConfig } from "./config-input.ts";
|
|
4
6
|
import { applyDeploymentEnv } from "./deployment-env.ts";
|
|
5
7
|
import { BlumeError, diagnosticsFromZod } from "./diagnostics.ts";
|
|
@@ -68,7 +70,8 @@ import type { Diagnostic } from "./types.ts";
|
|
|
68
70
|
* **Reference docs**
|
|
69
71
|
* - `openapi` — native OpenAPI reference: one real page per operation, woven
|
|
70
72
|
* into the sidebar and search. Point `sources`/`spec` at your spec.
|
|
71
|
-
* - `asyncapi` — AsyncAPI reference
|
|
73
|
+
* - `asyncapi` — native AsyncAPI reference with the same treatment; 2.x specs
|
|
74
|
+
* are normalized to 3.x automatically.
|
|
72
75
|
*
|
|
73
76
|
* **Search & AI**
|
|
74
77
|
* - `search` — search backend `provider` (`orama` by default; `pagefind`,
|
|
@@ -161,6 +164,15 @@ export interface ConfigLoadResult {
|
|
|
161
164
|
|
|
162
165
|
const importConfigModule = createModuleLoader();
|
|
163
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The slice of a user config module probed before schema defaults apply:
|
|
169
|
+
* whether `theme.fonts` was actually set. `looseObject` keeps every other key
|
|
170
|
+
* out of scope; a non-object at either level simply fails the probe.
|
|
171
|
+
*/
|
|
172
|
+
const themeFontsProbeSchema = z.looseObject({
|
|
173
|
+
theme: z.looseObject({ fonts: z.unknown() }).optional(),
|
|
174
|
+
});
|
|
175
|
+
|
|
164
176
|
/**
|
|
165
177
|
* Load and validate the project config. When no config file exists, schema
|
|
166
178
|
* defaults produce a fully resolved config so the zero-boilerplate path works.
|
|
@@ -176,11 +188,14 @@ export const loadConfig = async (
|
|
|
176
188
|
): Promise<ConfigLoadResult> => {
|
|
177
189
|
const configFile = findConfigFile(root);
|
|
178
190
|
|
|
179
|
-
let raw: unknown
|
|
191
|
+
let raw: unknown;
|
|
180
192
|
if (configFile) {
|
|
181
193
|
try {
|
|
182
194
|
raw = await importConfigModule(configFile);
|
|
183
195
|
} catch (error) {
|
|
196
|
+
// SAFETY: the module loader rejects with the thrown load/parse failure,
|
|
197
|
+
// which Node surfaces as an Error; a non-Error rejection only degrades
|
|
198
|
+
// the interpolated message.
|
|
184
199
|
throw new BlumeError({
|
|
185
200
|
code: "BLUME_CONFIG_LOAD_FAILED",
|
|
186
201
|
file: configFile,
|
|
@@ -191,11 +206,9 @@ export const loadConfig = async (
|
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
// Read before parsing: schema defaults erase the set-vs-defaulted distinction.
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
(raw as { theme?: { fonts?: unknown } }).theme?.fonts !== undefined
|
|
198
|
-
);
|
|
209
|
+
const probe = themeFontsProbeSchema.safeParse(raw);
|
|
210
|
+
const themeFontsConfigured =
|
|
211
|
+
probe.success && probe.data.theme?.fonts !== undefined;
|
|
199
212
|
|
|
200
213
|
const parsed = blumeConfigSchema.safeParse(raw ?? {});
|
|
201
214
|
if (!parsed.success) {
|
package/src/core/content.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResolvedI18nConfig } from "./schema.ts";
|
|
1
|
+
import type { ResolvedI18nConfig, ResolvedVersionsConfig } from "./schema.ts";
|
|
2
2
|
import { filesystemSource } from "./sources/filesystem.ts";
|
|
3
3
|
import { normalizeEntry } from "./sources/normalize.ts";
|
|
4
4
|
import type { Diagnostic, PageRecord } from "./types.ts";
|
|
@@ -24,6 +24,7 @@ export const discoverContent = async (options: {
|
|
|
24
24
|
defaultType: string;
|
|
25
25
|
basePath?: string;
|
|
26
26
|
i18n?: ResolvedI18nConfig;
|
|
27
|
+
versions?: ResolvedVersionsConfig;
|
|
27
28
|
}): Promise<{ pages: PageRecord[]; diagnostics: Diagnostic[] }> => {
|
|
28
29
|
const source = filesystemSource({
|
|
29
30
|
exclude: options.exclude,
|
|
@@ -43,6 +44,7 @@ export const discoverContent = async (options: {
|
|
|
43
44
|
defaultType: options.defaultType,
|
|
44
45
|
i18n: options.i18n,
|
|
45
46
|
source: { name: source.name, prefix: source.prefix, staged: false },
|
|
47
|
+
versions: options.versions,
|
|
46
48
|
});
|
|
47
49
|
pages.push(...normalized.pages);
|
|
48
50
|
diagnostics.push(...normalized.diagnostics);
|
package/src/core/data.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { FontHead } from "../theme/fonts.ts";
|
|
1
2
|
import type { UIStrings } from "./i18n-ui.ts";
|
|
2
3
|
import type { ResolvedConfig, SearchProvider } from "./schema.ts";
|
|
3
|
-
import type { Navigation, RouteAlternate } from "./types.ts";
|
|
4
|
+
import type { Navigation, RouteAlternate, VersionAlternate } from "./types.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The shape of the `blume:data` virtual module — the resolved, serializable
|
|
@@ -85,6 +86,14 @@ export interface BlumeRoute {
|
|
|
85
86
|
locale: string;
|
|
86
87
|
path: string;
|
|
87
88
|
title: string;
|
|
89
|
+
/** Resolved docs version (`""` for the current docs). */
|
|
90
|
+
version: string;
|
|
91
|
+
/**
|
|
92
|
+
* Versions this logical page exists in within this route's locale — the
|
|
93
|
+
* current version first, then archived versions in configured order. Empty
|
|
94
|
+
* when versioning is off.
|
|
95
|
+
*/
|
|
96
|
+
versionAlternates: VersionAlternate[];
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
/** Site-wide settings derived from `blume.config` — the `config` field of {@link BlumeData}. */
|
|
@@ -161,6 +170,8 @@ export interface BlumeDataConfig {
|
|
|
161
170
|
* WebMCP in-page tools (`ai.webmcp`), plus whether llms.txt exists for the
|
|
162
171
|
* list tool to fetch (`ai.llmsTxt.enabled`).
|
|
163
172
|
*/
|
|
173
|
+
/** Docs versioning config; `null` when the site is unversioned. */
|
|
174
|
+
versions: NonNullable<ResolvedConfig["versions"]> | null;
|
|
164
175
|
webmcp: { enabled: boolean; llms: boolean };
|
|
165
176
|
/** X (Twitter) attribution: the site's account, and a default creator. */
|
|
166
177
|
x: { creator?: string; handle?: string };
|
|
@@ -182,12 +193,17 @@ export interface BlumeClientData {
|
|
|
182
193
|
export interface BlumeData {
|
|
183
194
|
config: BlumeDataConfig;
|
|
184
195
|
feeds: BlumeFeed[];
|
|
185
|
-
/**
|
|
186
|
-
fontCssVars:
|
|
196
|
+
/** Configured fonts for the head: CSS variable + preload weights per family. */
|
|
197
|
+
fontCssVars: FontHead[];
|
|
187
198
|
/** Sidebar + tab tree for the default locale. */
|
|
188
199
|
navigation: Navigation;
|
|
189
200
|
/** Per-locale navigation trees, keyed by locale code (empty without i18n). */
|
|
190
201
|
navigationByLocale: Record<string, Navigation>;
|
|
202
|
+
/**
|
|
203
|
+
* Per-archived-version navigation trees, keyed by version id and then locale
|
|
204
|
+
* code (`""` on a single-locale site). Empty when versioning is off.
|
|
205
|
+
*/
|
|
206
|
+
navigationByVersion: Record<string, Record<string, Navigation>>;
|
|
191
207
|
routes: BlumeRoute[];
|
|
192
208
|
/** Resolved UI strings for the default locale. */
|
|
193
209
|
ui: UIStrings;
|
|
@@ -18,6 +18,10 @@ export interface IslandDescriptor {
|
|
|
18
18
|
export type ComponentOverride = ComponentReference | IslandDescriptor;
|
|
19
19
|
|
|
20
20
|
/** User-authored component overrides, grouped by surface. */
|
|
21
|
+
// oxlint-disable anti-slop/no-unsafe-dictionary-type -- `ComponentOverride` is untyped by
|
|
22
|
+
// design: user configs pass imported components from any framework (React
|
|
23
|
+
// functions, Svelte classes, Vue SFC objects), which share no structural type.
|
|
24
|
+
// `resolveSlot` and the generated components map are the runtime boundary.
|
|
21
25
|
export interface ComponentOverrides {
|
|
22
26
|
/**
|
|
23
27
|
* Interactive framework components made available in every `.mdx` page. Like
|
|
@@ -31,6 +35,7 @@ export interface ComponentOverrides {
|
|
|
31
35
|
/** MDX component map overrides (`Callout`, `Card`, ...). */
|
|
32
36
|
mdx?: Record<string, ComponentOverride>;
|
|
33
37
|
}
|
|
38
|
+
// oxlint-enable anti-slop/no-unsafe-dictionary-type
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
41
|
* Identity helper for authoring `components.ts`. Provides type inference and a
|
package/src/core/diagnostics.ts
CHANGED
|
@@ -27,41 +27,44 @@ const DOCS_CONTENT_SOURCES = "/docs/content/sources";
|
|
|
27
27
|
const DOCS_CONTENT_NAVIGATION = "/docs/content/navigation";
|
|
28
28
|
|
|
29
29
|
/** Diagnostic code → the docs page that explains it. */
|
|
30
|
-
const DOCS_PATHS
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
30
|
+
const DOCS_PATHS = new Map(
|
|
31
|
+
Object.entries({
|
|
32
|
+
BLUME_ADAPTER_REQUIRED: DOCS_DEPLOYMENT,
|
|
33
|
+
BLUME_ASSETS_UNCHECKED: DOCS_REFERENCE_CLI,
|
|
34
|
+
BLUME_ASSET_FETCH_FAILED: DOCS_CONTENT_SOURCES,
|
|
35
|
+
BLUME_BROKEN_ANCHOR: DOCS_REFERENCE_CLI,
|
|
36
|
+
BLUME_BROKEN_ASSET: DOCS_REFERENCE_CLI,
|
|
37
|
+
BLUME_BROKEN_LINK: DOCS_REFERENCE_CLI,
|
|
38
|
+
BLUME_CONFIG_INVALID: "/docs/configuration",
|
|
39
|
+
BLUME_CONFIG_LOAD_FAILED: "/docs/configuration",
|
|
40
|
+
BLUME_CONTENT_ROOT_MISSING: DOCS_CONTENT_SOURCES,
|
|
41
|
+
BLUME_DEAD_LINK: DOCS_REFERENCE_CLI,
|
|
42
|
+
BLUME_DUPLICATE_ROUTE: DOCS_CONTENT_NAVIGATION,
|
|
43
|
+
BLUME_DUPLICATE_SIDEBAR_ORDER: DOCS_CONTENT_NAVIGATION,
|
|
44
|
+
BLUME_FRONTMATTER_INVALID: "/docs/reference/frontmatter",
|
|
45
|
+
BLUME_META_INVALID: "/docs/content/meta",
|
|
46
|
+
BLUME_META_LOAD_FAILED: "/docs/content/meta",
|
|
47
|
+
BLUME_MISSING_SECRET: DOCS_DEPLOYMENT,
|
|
48
|
+
BLUME_NAV_DUPLICATE_LABEL: DOCS_CONTENT_NAVIGATION,
|
|
49
|
+
BLUME_NAV_HIDDEN_IN_SIDEBAR: DOCS_CONTENT_NAVIGATION,
|
|
50
|
+
BLUME_NAV_INDEX_TITLE_MISMATCH: DOCS_CONTENT_NAVIGATION,
|
|
51
|
+
BLUME_NAV_MISSING_PAGE: DOCS_CONTENT_NAVIGATION,
|
|
52
|
+
BLUME_NODE_VERSION: "/docs/quickstart",
|
|
53
|
+
BLUME_SERVER_FEATURE_REQUIRED: DOCS_DEPLOYMENT,
|
|
54
|
+
BLUME_SIDEBAR_DISPLAY_IGNORED: DOCS_CONTENT_NAVIGATION,
|
|
55
|
+
BLUME_SOURCE_FETCH_FAILED: DOCS_CONTENT_SOURCES,
|
|
56
|
+
BLUME_SOURCE_MISCONFIGURED: DOCS_CONTENT_SOURCES,
|
|
57
|
+
BLUME_SOURCE_OFFLINE: DOCS_CONTENT_SOURCES,
|
|
58
|
+
BLUME_SOURCE_SDK_MISSING: DOCS_CONTENT_SOURCES,
|
|
59
|
+
BLUME_SOURCE_UNAVAILABLE: DOCS_CONTENT_SOURCES,
|
|
60
|
+
BLUME_UNKNOWN_COMPONENT: "/docs/configuration/customization",
|
|
61
|
+
BLUME_UNKNOWN_ICON: DOCS_CONTENT_NAVIGATION,
|
|
62
|
+
})
|
|
63
|
+
);
|
|
61
64
|
|
|
62
65
|
/** The docs URL that explains a diagnostic code, if one is mapped. */
|
|
63
66
|
export const resolveDocsUrl = (code: string): string | undefined => {
|
|
64
|
-
const path = DOCS_PATHS
|
|
67
|
+
const path = DOCS_PATHS.get(code);
|
|
65
68
|
return path ? `${DOCS_BASE}${path}` : undefined;
|
|
66
69
|
};
|
|
67
70
|
|
|
@@ -75,6 +78,10 @@ const REGEXP_SPECIAL = /[$()*+.?[\\\]^{|}]/gu;
|
|
|
75
78
|
const escapeRegExp = (value: string): string =>
|
|
76
79
|
value.replaceAll(REGEXP_SPECIAL, String.raw`\$&`);
|
|
77
80
|
|
|
81
|
+
/** A key segment scans the source text; an array index has no key to find. */
|
|
82
|
+
const isKeySegment = (segment: string | number): segment is string =>
|
|
83
|
+
typeof segment === "string";
|
|
84
|
+
|
|
78
85
|
/**
|
|
79
86
|
* Best-effort source position for a Zod issue path (e.g. `["seo", "title"]`) in
|
|
80
87
|
* the raw config / frontmatter text. Narrows key-by-key — finding each string
|
|
@@ -86,9 +93,9 @@ const stepSegment = (
|
|
|
86
93
|
source: string,
|
|
87
94
|
segment: string | number,
|
|
88
95
|
cursor: number
|
|
89
|
-
)
|
|
96
|
+
) => {
|
|
90
97
|
// A non-string path segment (array index) is skipped without moving on.
|
|
91
|
-
if (
|
|
98
|
+
if (!isKeySegment(segment)) {
|
|
92
99
|
return { index: -1, next: cursor, stop: false };
|
|
93
100
|
}
|
|
94
101
|
// The negative lookbehind keeps a segment like `title` from matching the
|
|
@@ -247,10 +254,11 @@ export const formatDiagnostic = (
|
|
|
247
254
|
export const hasErrors = (diagnostics: Diagnostic[]): boolean =>
|
|
248
255
|
diagnostics.some((d) => d.severity === "error");
|
|
249
256
|
|
|
250
|
-
export const countBySeverity = (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
export const countBySeverity = (diagnostics: Diagnostic[]) => {
|
|
258
|
+
const counts = { error: 0, info: 0, warning: 0 } satisfies Record<
|
|
259
|
+
Diagnostic["severity"],
|
|
260
|
+
number
|
|
261
|
+
>;
|
|
254
262
|
for (const diagnostic of diagnostics) {
|
|
255
263
|
counts[diagnostic.severity] += 1;
|
|
256
264
|
}
|