blume 0.7.0 → 1.0.0
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 +666 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +1852 -380
- package/dist/cli/index.js.map +98 -91
- package/dist/types/ai/component-markdown.d.ts +34 -0
- package/dist/types/components/content/youtube.d.ts +18 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +47 -2
- package/dist/types/core/config.d.ts +3 -2
- package/dist/types/core/data.d.ts +7 -0
- package/dist/types/core/i18n-ui.d.ts +526 -132
- package/dist/types/core/schema.d.ts +293 -146
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/dist/types/seo/x-handle.d.ts +12 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +8 -8
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/api-reference.mdx +1 -1
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +49 -10
- package/docs/configuration/customization.mdx +11 -0
- package/docs/configuration/export.mdx +1 -1
- package/docs/configuration/index.mdx +27 -3
- package/docs/configuration/seo.mdx +35 -5
- package/docs/content/components.mdx +2 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +6 -4
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +9 -7
- package/docs/reference/frontmatter.mdx +1 -1
- package/package.json +22 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +9 -8
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +135 -26
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +25 -4
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +21 -7
- package/src/ai/mcp/tools.ts +1 -1
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +11 -1
- package/src/astro/generate.ts +77 -45
- package/src/astro/integration.ts +1 -1
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +81 -19
- package/src/astro/templates.ts +150 -19
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +120 -23
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +26 -5
- package/src/cli/commands/eject.ts +47 -19
- package/src/cli/commands/init.ts +120 -180
- package/src/cli/commands/preview.ts +4 -1
- package/src/cli/commands/validate.ts +43 -2
- package/src/cli/dev-lock.ts +8 -4
- package/src/cli/eject-scripts.ts +72 -0
- package/src/cli/env.ts +15 -5
- package/src/cli/init/questions.ts +158 -0
- package/src/cli/init/scaffold.ts +380 -0
- package/src/cli/internal-error.ts +9 -4
- package/src/cli/prepare.ts +3 -2
- package/src/components/Icon.astro +2 -1
- package/src/components/content/AccordionItem.astro +23 -4
- package/src/components/content/Badge.astro +3 -1
- package/src/components/content/Card.astro +4 -2
- package/src/components/content/Step.astro +10 -1
- package/src/components/content/Tabs.astro +15 -3
- package/src/components/content/Tile.astro +2 -1
- package/src/components/content/Tooltip.astro +3 -1
- package/src/components/content/Update.astro +9 -2
- package/src/components/content/auto-type-table.ts +7 -1
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/mermaid-element.ts +7 -2
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +86 -11
- package/src/components/islands/hooks.ts +28 -8
- package/src/components/layout/Banner.astro +10 -2
- package/src/components/layout/Breadcrumbs.astro +11 -2
- package/src/components/layout/Header.astro +13 -4
- package/src/components/layout/Logo.astro +11 -3
- package/src/components/layout/NavTree.astro +19 -5
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageLayout.astro +85 -9
- package/src/components/layout/Pagination.astro +10 -4
- package/src/components/layout/ReferenceLayout.astro +20 -2
- package/src/components/layout/RootLayout.astro +142 -12
- package/src/components/layout/Search.astro +117 -27
- package/src/components/layout/search/algolia.ts +11 -2
- package/src/components/layout/search/endpoint.ts +11 -5
- package/src/components/layout/search/orama-cloud.ts +8 -2
- package/src/components/layout/search/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +1 -1
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +1 -1
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/core/base-path.ts +24 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/config-input.ts +48 -2
- package/src/core/config.ts +3 -2
- package/src/core/data.ts +4 -0
- package/src/core/frontmatter.ts +7 -0
- package/src/core/graph.ts +15 -5
- package/src/core/i18n-ui.ts +54 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/last-modified.ts +13 -6
- package/src/core/links.ts +32 -8
- package/src/core/navigation.ts +29 -4
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +15 -6
- package/src/core/schema.ts +71 -2
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/mdx-remote.ts +23 -14
- package/src/core/sources/normalize.ts +152 -50
- package/src/core/sources/notion.ts +8 -8
- package/src/core/ui-packs/ar.ts +8 -0
- package/src/core/ui-packs/bg.ts +8 -0
- package/src/core/ui-packs/bn.ts +8 -0
- package/src/core/ui-packs/ca.ts +8 -0
- package/src/core/ui-packs/cs.ts +8 -0
- package/src/core/ui-packs/da.ts +8 -0
- package/src/core/ui-packs/de.ts +8 -0
- package/src/core/ui-packs/el.ts +8 -0
- package/src/core/ui-packs/es.ts +8 -0
- package/src/core/ui-packs/fa.ts +8 -0
- package/src/core/ui-packs/fi.ts +8 -0
- package/src/core/ui-packs/fr.ts +9 -1
- package/src/core/ui-packs/he.ts +8 -0
- package/src/core/ui-packs/hi.ts +8 -0
- package/src/core/ui-packs/hr.ts +8 -0
- package/src/core/ui-packs/hu.ts +8 -0
- package/src/core/ui-packs/id.ts +8 -0
- package/src/core/ui-packs/it.ts +8 -0
- package/src/core/ui-packs/ja.ts +8 -0
- package/src/core/ui-packs/ko.ts +8 -0
- package/src/core/ui-packs/nl.ts +8 -0
- package/src/core/ui-packs/no.ts +8 -0
- package/src/core/ui-packs/pl.ts +8 -0
- package/src/core/ui-packs/pt-br.ts +8 -0
- package/src/core/ui-packs/pt.ts +8 -0
- package/src/core/ui-packs/ro.ts +8 -0
- package/src/core/ui-packs/ru.ts +8 -0
- package/src/core/ui-packs/sk.ts +8 -0
- package/src/core/ui-packs/sr.ts +8 -0
- package/src/core/ui-packs/sv.ts +8 -0
- package/src/core/ui-packs/th.ts +8 -0
- package/src/core/ui-packs/tr.ts +8 -0
- package/src/core/ui-packs/uk.ts +8 -0
- package/src/core/ui-packs/vi.ts +8 -0
- package/src/core/ui-packs/zh-tw.ts +8 -0
- package/src/core/ui-packs/zh.ts +8 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +7 -2
- package/src/deploy/sitemap.ts +53 -11
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +10 -8
- package/src/markdown/index.ts +15 -3
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/package-commands.ts +10 -4
- package/src/og/card.ts +4 -2
- package/src/og/dimensions.ts +12 -0
- package/src/openapi/model.ts +12 -4
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +38 -8
- package/src/openapi/render-mdx.ts +62 -1
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +184 -12
- package/src/registry/registry.ts +0 -3
- package/src/search/documents.ts +34 -2
- package/src/seo/jsonld.ts +20 -13
- package/src/seo/x-handle.ts +18 -0
|
@@ -31,7 +31,7 @@ export const createSearch = (opts: {
|
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
33
|
});
|
|
34
|
-
return async (query) => {
|
|
34
|
+
return async (query, options) => {
|
|
35
35
|
const response = await client
|
|
36
36
|
.collections<TypesenseRecord>(opts.collection)
|
|
37
37
|
.documents()
|
|
@@ -40,6 +40,9 @@ export const createSearch = (opts: {
|
|
|
40
40
|
per_page: SEARCH_LIMIT,
|
|
41
41
|
q: query,
|
|
42
42
|
query_by: "title,description,content",
|
|
43
|
+
// The sync marks `locale` as a facet so an i18n site can scope
|
|
44
|
+
// hosted results to the active language.
|
|
45
|
+
...(options?.locale && { filter_by: `locale:=${options.locale}` }),
|
|
43
46
|
},
|
|
44
47
|
{}
|
|
45
48
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client
|
|
2
|
+
* Client behavior for the `<blume-toc>` custom element wrapping each "On this
|
|
3
3
|
* page" list. As the reader scrolls, it marks the link for the section currently
|
|
4
4
|
* in view with `aria-current="location"` — styled via Tailwind `aria-[current]`
|
|
5
5
|
* variants in RootLayout — giving the table of contents a live scrollspy.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
+
import { withBase } from "../islands/base-path.ts";
|
|
3
4
|
import MethodBadge from "./MethodBadge.astro";
|
|
4
5
|
|
|
5
6
|
// The operation-link list for one tag section of an API overview page. The
|
|
@@ -25,7 +26,7 @@ const operations = Object.values(specs[source]?.operations ?? {}).filter(
|
|
|
25
26
|
<li>
|
|
26
27
|
<a
|
|
27
28
|
class="flex items-center gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
|
|
28
|
-
href={operation.route}
|
|
29
|
+
href={withBase(operation.route)}
|
|
29
30
|
>
|
|
30
31
|
<MethodBadge method={operation.method} />
|
|
31
32
|
<span class="font-medium text-foreground text-sm">
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
mergeParameters,
|
|
5
|
+
type ParameterLike,
|
|
6
|
+
resolveComponentRef,
|
|
7
|
+
type SchemaLike,
|
|
8
|
+
} from "./helpers.ts";
|
|
4
9
|
import { buildRequestSample, sampleLanguages } from "./snippets.ts";
|
|
5
10
|
import MethodBadge from "./MethodBadge.astro";
|
|
6
11
|
import ParametersTable from "./ParametersTable.astro";
|
|
@@ -13,36 +18,31 @@ interface Props {
|
|
|
13
18
|
id: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
interface
|
|
21
|
+
interface MediaTypeLike {
|
|
22
|
+
schema?: SchemaLike;
|
|
23
|
+
example?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RequestBodyLike {
|
|
17
27
|
$ref?: string;
|
|
18
|
-
name?: string;
|
|
19
|
-
in?: string;
|
|
20
28
|
description?: string;
|
|
21
29
|
required?: boolean;
|
|
22
|
-
|
|
23
|
-
schema?: SchemaLike;
|
|
24
|
-
example?: unknown;
|
|
30
|
+
content?: Record<string, MediaTypeLike>;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
interface
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
interface ResponseLike {
|
|
34
|
+
$ref?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
content?: Record<string, MediaTypeLike>;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
interface FullOperation {
|
|
33
40
|
summary?: string;
|
|
34
41
|
description?: string;
|
|
35
42
|
deprecated?: boolean;
|
|
36
|
-
parameters?:
|
|
37
|
-
requestBody?:
|
|
38
|
-
|
|
39
|
-
required?: boolean;
|
|
40
|
-
content?: Record<string, MediaTypeLike>;
|
|
41
|
-
};
|
|
42
|
-
responses?: Record<
|
|
43
|
-
string,
|
|
44
|
-
{ description?: string; content?: Record<string, MediaTypeLike> }
|
|
45
|
-
>;
|
|
43
|
+
parameters?: ParameterLike[];
|
|
44
|
+
requestBody?: RequestBodyLike;
|
|
45
|
+
responses?: Record<string, ResponseLike>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const { source, id } = Astro.props;
|
|
@@ -50,10 +50,15 @@ const spec = specs[source];
|
|
|
50
50
|
const ref = spec?.operations[id];
|
|
51
51
|
|
|
52
52
|
const doc = (spec?.document ?? {}) as {
|
|
53
|
-
paths?: Record<
|
|
53
|
+
paths?: Record<
|
|
54
|
+
string,
|
|
55
|
+
Record<string, unknown> & { parameters?: ParameterLike[] }
|
|
56
|
+
>;
|
|
54
57
|
components?: {
|
|
55
58
|
schemas?: Record<string, SchemaLike>;
|
|
56
|
-
parameters?: Record<string,
|
|
59
|
+
parameters?: Record<string, ParameterLike>;
|
|
60
|
+
requestBodies?: Record<string, RequestBodyLike>;
|
|
61
|
+
responses?: Record<string, ResponseLike>;
|
|
57
62
|
};
|
|
58
63
|
servers?: { url?: string }[];
|
|
59
64
|
};
|
|
@@ -63,24 +68,26 @@ const operation = (
|
|
|
63
68
|
) as FullOperation | undefined;
|
|
64
69
|
|
|
65
70
|
const schemas = doc.components?.schemas ?? {};
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
const components = doc.components ?? {};
|
|
72
|
+
const params = mergeParameters(
|
|
73
|
+
pathItem?.parameters,
|
|
74
|
+
operation?.parameters,
|
|
75
|
+
components
|
|
76
|
+
);
|
|
77
|
+
const requestBody = operation?.requestBody
|
|
78
|
+
? resolveComponentRef(operation.requestBody, components, "requestBodies")
|
|
79
|
+
: undefined;
|
|
80
|
+
const responses = Object.fromEntries(
|
|
81
|
+
Object.entries(operation?.responses ?? {}).map(([status, response]) => [
|
|
82
|
+
status,
|
|
83
|
+
resolveComponentRef(response, components, "responses"),
|
|
84
|
+
])
|
|
85
|
+
);
|
|
79
86
|
|
|
80
87
|
const sample =
|
|
81
88
|
ref && operation
|
|
82
89
|
? buildRequestSample(
|
|
83
|
-
{ parameters: params, requestBody
|
|
90
|
+
{ parameters: params, requestBody },
|
|
84
91
|
ref.method,
|
|
85
92
|
ref.path,
|
|
86
93
|
doc.servers ?? [],
|
|
@@ -109,17 +116,17 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
|
109
116
|
<div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
|
|
110
117
|
<div>
|
|
111
118
|
<ParametersTable parameters={params} schemas={schemas} />
|
|
112
|
-
{
|
|
119
|
+
{requestBody && (
|
|
113
120
|
<RequestBody
|
|
114
121
|
expandAll={spec.expandSchemas}
|
|
115
|
-
requestBody={
|
|
122
|
+
requestBody={requestBody}
|
|
116
123
|
schemas={schemas}
|
|
117
124
|
/>
|
|
118
125
|
)}
|
|
119
126
|
{operation.responses && (
|
|
120
127
|
<Responses
|
|
121
128
|
expandAll={spec.expandSchemas}
|
|
122
|
-
responses={
|
|
129
|
+
responses={responses}
|
|
123
130
|
schemas={schemas}
|
|
124
131
|
/>
|
|
125
132
|
)}
|
|
@@ -128,7 +135,7 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
|
128
135
|
<div class="xl:sticky xl:top-24 xl:self-start">
|
|
129
136
|
<RequestPanel
|
|
130
137
|
languages={languages}
|
|
131
|
-
responses={
|
|
138
|
+
responses={responses}
|
|
132
139
|
sample={sample}
|
|
133
140
|
schemas={schemas}
|
|
134
141
|
/>
|
|
@@ -24,7 +24,7 @@ interface Props {
|
|
|
24
24
|
|
|
25
25
|
const { sample, languages, responses, schemas } = Astro.props;
|
|
26
26
|
|
|
27
|
-
// A `.prose` wrapper gives Shiki its scoped token
|
|
27
|
+
// A `.prose` wrapper gives Shiki its scoped token colors; the global style at
|
|
28
28
|
// the foot of this file strips the standalone code block's own box (border,
|
|
29
29
|
// injected copy button, language label) so the code sits flush inside the one
|
|
30
30
|
// panel border.
|
|
@@ -39,8 +39,71 @@ export interface SchemaLike {
|
|
|
39
39
|
[key: string]: unknown;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** A permissive view of an operation parameter — only the fields we render. */
|
|
43
|
+
export interface ParameterLike {
|
|
44
|
+
$ref?: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
in?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
required?: boolean;
|
|
49
|
+
deprecated?: boolean;
|
|
50
|
+
schema?: SchemaLike;
|
|
51
|
+
example?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
const REF_PATTERN = /#\/components\/schemas\/(?<name>[^/]+)$/u;
|
|
43
55
|
|
|
56
|
+
const COMPONENT_REF = /#\/components\/(?<section>[^/]+)\/(?<name>[^/]+)$/u;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve one level of `$ref` against a named `components` section
|
|
60
|
+
* (`parameters`, `requestBodies`, `responses`). Mirrors {@link resolveSchema}:
|
|
61
|
+
* an unknown ref — or one pointing into a different section — is returned
|
|
62
|
+
* as-is.
|
|
63
|
+
*/
|
|
64
|
+
export const resolveComponentRef = <T extends { $ref?: string }>(
|
|
65
|
+
node: T,
|
|
66
|
+
components: Record<string, unknown> | undefined,
|
|
67
|
+
section: string
|
|
68
|
+
): T => {
|
|
69
|
+
if (typeof node.$ref !== "string") {
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
const groups = COMPONENT_REF.exec(node.$ref)?.groups;
|
|
73
|
+
if (groups?.section !== section) {
|
|
74
|
+
return node;
|
|
75
|
+
}
|
|
76
|
+
const table = components?.[section] as Record<string, T> | undefined;
|
|
77
|
+
return table?.[groups.name ?? ""] ?? node;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Path-level and operation-level parameters merged into one render list.
|
|
82
|
+
* `$ref`s resolve against `components.parameters` first; then an operation
|
|
83
|
+
* parameter overrides a path-level one with the same `name` + `in` (the
|
|
84
|
+
* OpenAPI override rule), so a re-declared parameter appears once.
|
|
85
|
+
*/
|
|
86
|
+
export const mergeParameters = (
|
|
87
|
+
pathParameters: ParameterLike[] | undefined,
|
|
88
|
+
operationParameters: ParameterLike[] | undefined,
|
|
89
|
+
components?: Record<string, unknown>
|
|
90
|
+
): ParameterLike[] => {
|
|
91
|
+
const merged = new Map<string, ParameterLike>();
|
|
92
|
+
let position = 0;
|
|
93
|
+
for (const raw of [
|
|
94
|
+
...(pathParameters ?? []),
|
|
95
|
+
...(operationParameters ?? []),
|
|
96
|
+
]) {
|
|
97
|
+
const param = resolveComponentRef(raw, components, "parameters");
|
|
98
|
+
// A nameless parameter is invalid per spec, but key it uniquely so it is
|
|
99
|
+
// still rendered rather than collapsing with other invalid entries.
|
|
100
|
+
const key = param.name ? `${param.in ?? ""}:${param.name}` : `#${position}`;
|
|
101
|
+
merged.set(key, param);
|
|
102
|
+
position += 1;
|
|
103
|
+
}
|
|
104
|
+
return [...merged.values()];
|
|
105
|
+
};
|
|
106
|
+
|
|
44
107
|
/** The display name of a `$ref`, e.g. `#/components/schemas/Pet` -> `Pet`. */
|
|
45
108
|
export const refName = (ref: string): string =>
|
|
46
109
|
REF_PATTERN.exec(ref)?.groups?.name ?? ref.split("/").at(-1) ?? ref;
|
|
@@ -166,7 +229,7 @@ export const objectProperties = (
|
|
|
166
229
|
/** Sentinel: no explicit example is declared on a schema. */
|
|
167
230
|
const NO_VALUE = Symbol("no-value");
|
|
168
231
|
|
|
169
|
-
/** The declared example/default/enum for a schema, or {@link NO_VALUE}. */
|
|
232
|
+
/** The declared example/const/default/enum for a schema, or {@link NO_VALUE}. */
|
|
170
233
|
const explicitExample = (schema: SchemaLike): unknown => {
|
|
171
234
|
if (schema.example !== undefined) {
|
|
172
235
|
return schema.example;
|
|
@@ -174,6 +237,11 @@ const explicitExample = (schema: SchemaLike): unknown => {
|
|
|
174
237
|
if (Array.isArray(schema.examples) && schema.examples.length > 0) {
|
|
175
238
|
return schema.examples[0];
|
|
176
239
|
}
|
|
240
|
+
// `const` is the schema's only valid value (the 3.1 discriminator idiom), so
|
|
241
|
+
// it outranks `default`/`enum` — either of those differing would be invalid.
|
|
242
|
+
if (schema.const !== undefined) {
|
|
243
|
+
return schema.const;
|
|
244
|
+
}
|
|
177
245
|
if (schema.default !== undefined) {
|
|
178
246
|
return schema.default;
|
|
179
247
|
}
|
|
@@ -202,8 +270,8 @@ const primitiveExample = (
|
|
|
202
270
|
|
|
203
271
|
/**
|
|
204
272
|
* Build a representative example value for a schema (honoring `example` /
|
|
205
|
-
* `default` / `enum` first). A `seen` set of `$ref`s guards against
|
|
206
|
-
* schemas that keeping refs intact allows.
|
|
273
|
+
* `const` / `default` / `enum` first). A `seen` set of `$ref`s guards against
|
|
274
|
+
* the circular schemas that keeping refs intact allows.
|
|
207
275
|
*/
|
|
208
276
|
export const exampleValue = (
|
|
209
277
|
schema: SchemaLike | undefined,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client
|
|
2
|
+
* Client behavior for the OpenAPI request/response panels. `<blume-panel-tabs>`
|
|
3
3
|
* switches the visible `[data-panel="key"]` region when a `[data-panel-tab="key"]`
|
|
4
4
|
* button is clicked, and an optional `[data-panel-copy]` button copies the active
|
|
5
5
|
* panel's text. Vanilla custom element — no framework, in keeping with the core
|
package/src/core/base-path.ts
CHANGED
|
@@ -51,6 +51,30 @@ export const withBasePath = (basePath: string, route: string): string => {
|
|
|
51
51
|
return route === "/" ? basePath : `${basePath}${route}`;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* {@link withBasePath} for the composed `deployment.base` + `basePath` stack
|
|
56
|
+
* (`/base` + `/docs` serves pages at `/base/docs/x`). The hand-written-base
|
|
57
|
+
* promise applies per layer: authors write `basePath` by hand (see
|
|
58
|
+
* `markdown/base-links.ts`), so a `/docs/x` link gains only the deployment base
|
|
59
|
+
* (`/base/docs/x`) rather than being double-prefixed to `/base/docs/docs/x`,
|
|
60
|
+
* and a route already under the full composite is returned unchanged.
|
|
61
|
+
*/
|
|
62
|
+
export const withComposedBasePath = (
|
|
63
|
+
deployBase: string,
|
|
64
|
+
basePath: string,
|
|
65
|
+
route: string
|
|
66
|
+
): string => {
|
|
67
|
+
const composed = `${deployBase}${basePath}`;
|
|
68
|
+
if (
|
|
69
|
+
composed &&
|
|
70
|
+
isInternalPath(route) &&
|
|
71
|
+
(route === composed || route.startsWith(`${composed}/`))
|
|
72
|
+
) {
|
|
73
|
+
return route;
|
|
74
|
+
}
|
|
75
|
+
return withBasePath(deployBase, withBasePath(basePath, route));
|
|
76
|
+
};
|
|
77
|
+
|
|
54
78
|
/**
|
|
55
79
|
* Remove `basePath` from the front of a route (`/docs/guide` -> `/guide`,
|
|
56
80
|
* `/docs` -> `/`). A route not under the base is returned unchanged. Inverse of
|
package/src/core/builtin-tags.ts
CHANGED
|
@@ -26,6 +26,8 @@ export const BUILTIN_MDX_TAGS = new Set<string>([
|
|
|
26
26
|
"Frame",
|
|
27
27
|
"GithubInfo",
|
|
28
28
|
"Icon",
|
|
29
|
+
// Conditionally imported by the catch-all, but `detectUsesMath` matches any
|
|
30
|
+
// authored `<Math`, so it is always wired wherever it can appear.
|
|
29
31
|
"Math",
|
|
30
32
|
"Operation",
|
|
31
33
|
"Panel",
|
package/src/core/config-input.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
|
|
3
|
+
import type { ComponentMarkdown } from "../ai/component-markdown.ts";
|
|
3
4
|
import type { FontSlug } from "../theme/fonts.ts";
|
|
4
5
|
import type {
|
|
5
6
|
blumeConfigSchema,
|
|
@@ -512,12 +513,46 @@ export interface AskConfig {
|
|
|
512
513
|
suggestions?: AskSuggestion[];
|
|
513
514
|
}
|
|
514
515
|
|
|
516
|
+
/** What the `llms.txt`/`llms-full.txt` files include. */
|
|
517
|
+
export interface LlmsTxtConfig {
|
|
518
|
+
/** Emit `llms.txt` and `llms-full.txt`. Defaults to `true`. */
|
|
519
|
+
enabled?: boolean;
|
|
520
|
+
/**
|
|
521
|
+
* Include the generated API reference pages (OpenAPI/AsyncAPI). Defaults to
|
|
522
|
+
* `true`; set `false` to keep a placeholder or example spec's pages out of
|
|
523
|
+
* the LLM-facing files.
|
|
524
|
+
*/
|
|
525
|
+
openapi?: boolean;
|
|
526
|
+
}
|
|
527
|
+
|
|
515
528
|
/** AI-facing features: the Ask AI assistant and an `llms.txt` manifest. */
|
|
516
529
|
export interface AiConfig {
|
|
517
530
|
/** The Ask AI chat assistant. */
|
|
518
531
|
ask?: AskConfig;
|
|
519
|
-
/**
|
|
520
|
-
|
|
532
|
+
/**
|
|
533
|
+
* Emit `llms.txt` (an index of the docs for LLMs). Defaults to `true`.
|
|
534
|
+
* The object form adds knobs for what the files include.
|
|
535
|
+
*/
|
|
536
|
+
llmsTxt?: boolean | LlmsTxtConfig;
|
|
537
|
+
/**
|
|
538
|
+
* Markdown serializers for custom components in agent-facing output (the
|
|
539
|
+
* `.md` mirror, `llms-full.txt`, MCP `get_page`), keyed by JSX name. Each
|
|
540
|
+
* receives the component's statically-evaluated `props` and downleveled
|
|
541
|
+
* `children` and returns replacement Markdown — or `null` to leave the JSX
|
|
542
|
+
* verbatim. A same-name entry replaces a built-in serializer.
|
|
543
|
+
*
|
|
544
|
+
* These live in `blume.config.ts` (which is executed at build time), not in
|
|
545
|
+
* `components.tsx` (which is only statically analyzed, never run).
|
|
546
|
+
*
|
|
547
|
+
* ```ts
|
|
548
|
+
* ai: {
|
|
549
|
+
* markdownComponents: {
|
|
550
|
+
* Chart: ({ props }) => ``,
|
|
551
|
+
* },
|
|
552
|
+
* }
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
markdownComponents?: Record<string, ComponentMarkdown>;
|
|
521
556
|
}
|
|
522
557
|
|
|
523
558
|
// ---------------------------------------------------------------------------
|
|
@@ -701,6 +736,17 @@ export interface SeoConfig {
|
|
|
701
736
|
sitemap?: boolean;
|
|
702
737
|
/** Emit schema.org JSON-LD in each page's `<head>`. Defaults to `true`. */
|
|
703
738
|
structuredData?: boolean;
|
|
739
|
+
/**
|
|
740
|
+
* X (Twitter) attribution for share cards. Handles may omit the `@`. The rest
|
|
741
|
+
* of the X card is read from the `og:*` tags, so these accounts are the only
|
|
742
|
+
* values X cannot infer.
|
|
743
|
+
*/
|
|
744
|
+
x?: {
|
|
745
|
+
/** Author account (`twitter:creator`); a page can override it via `seo.x.creator` frontmatter. */
|
|
746
|
+
creator?: string;
|
|
747
|
+
/** The site's own account (`twitter:site`), e.g. `@blume`. */
|
|
748
|
+
handle?: string;
|
|
749
|
+
};
|
|
704
750
|
}
|
|
705
751
|
|
|
706
752
|
// ---------------------------------------------------------------------------
|
package/src/core/config.ts
CHANGED
|
@@ -73,8 +73,9 @@ import type { Diagnostic } from "./types.ts";
|
|
|
73
73
|
* - `search` — search backend `provider` (`orama` by default; `pagefind`,
|
|
74
74
|
* `algolia`, `typesense`, `orama-cloud`, `mixedbread`, or `none`) plus its
|
|
75
75
|
* credential block.
|
|
76
|
-
* - `ai` — `ask` (the Ask AI chat endpoint and its provider/model)
|
|
77
|
-
* (emit `llms.txt`)
|
|
76
|
+
* - `ai` — `ask` (the Ask AI chat endpoint and its provider/model), `llmsTxt`
|
|
77
|
+
* (emit `llms.txt`), and `markdownComponents` (Markdown serializers for
|
|
78
|
+
* custom components in agent-facing output).
|
|
78
79
|
* - `mcp` — expose the docs as an MCP server for connecting agents.
|
|
79
80
|
*
|
|
80
81
|
* **SEO, feeds & analytics**
|
package/src/core/data.ts
CHANGED
|
@@ -93,6 +93,8 @@ export interface BlumeDataConfig {
|
|
|
93
93
|
suggestions: NonNullable<ResolvedConfig["ai"]["ask"]>["suggestions"];
|
|
94
94
|
} | null;
|
|
95
95
|
banner: BlumeBanner | null;
|
|
96
|
+
/** Site-wide route mount point, normalized to `""` or `/seg` (see config). */
|
|
97
|
+
basePath: string;
|
|
96
98
|
/** `markdown.codeBlocks.theme`: light/dark Shiki themes for code surfaces. */
|
|
97
99
|
codeThemes: ResolvedConfig["markdown"]["codeBlocks"]["theme"];
|
|
98
100
|
/** `markdown.code.wrap`: wrap long code lines instead of scrolling. */
|
|
@@ -118,6 +120,8 @@ export interface BlumeDataConfig {
|
|
|
118
120
|
title: string;
|
|
119
121
|
/** Table-of-contents settings: whether to show it and the heading range. */
|
|
120
122
|
toc: ResolvedConfig["toc"];
|
|
123
|
+
/** X (Twitter) attribution: the site's account, and a default creator. */
|
|
124
|
+
x: { creator?: string; handle?: string };
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
/**
|
package/src/core/frontmatter.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { dump, load } from "js-yaml";
|
|
|
11
11
|
|
|
12
12
|
type MatterInput = Parameters<typeof baseMatter>[0];
|
|
13
13
|
type MatterOptions = Parameters<typeof baseMatter>[1];
|
|
14
|
+
type ReadArgs = Parameters<typeof baseMatter.read>;
|
|
14
15
|
type StringifyArgs = Parameters<typeof baseMatter.stringify>;
|
|
15
16
|
|
|
16
17
|
const yamlEngine = {
|
|
@@ -27,11 +28,17 @@ const withYamlEngine = <O>(options: O): O =>
|
|
|
27
28
|
},
|
|
28
29
|
}) as O;
|
|
29
30
|
|
|
31
|
+
// Every helper that parses or emits YAML (`read`, `stringify`) must be
|
|
32
|
+
// re-wrapped here — Object.assign copies gray-matter's own helpers, which use
|
|
33
|
+
// its default `safeLoad` engine and would reintroduce the crash. `test` only
|
|
34
|
+
// checks for a delimiter, so the copied original is safe.
|
|
30
35
|
const matter = Object.assign(
|
|
31
36
|
(input: MatterInput, options?: MatterOptions) =>
|
|
32
37
|
baseMatter(input, withYamlEngine(options)),
|
|
33
38
|
baseMatter,
|
|
34
39
|
{
|
|
40
|
+
read: (filepath: ReadArgs[0], options?: ReadArgs[1]) =>
|
|
41
|
+
baseMatter.read(filepath, withYamlEngine(options)),
|
|
35
42
|
stringify: (
|
|
36
43
|
file: StringifyArgs[0],
|
|
37
44
|
data: StringifyArgs[1],
|
package/src/core/graph.ts
CHANGED
|
@@ -87,13 +87,19 @@ const buildLocaleNavigation = (
|
|
|
87
87
|
options: BuildContentGraphOptions,
|
|
88
88
|
i18n: ResolvedI18nConfig
|
|
89
89
|
): Navigation => {
|
|
90
|
-
// Localize internal tab paths
|
|
91
|
-
// (e.g. `/docs` -> `/fr/docs`);
|
|
90
|
+
// Localize internal tab paths — the tab's own and its dropdown items' — so a
|
|
91
|
+
// header tab points to its in-locale route (e.g. `/docs` -> `/fr/docs`);
|
|
92
|
+
// external paths pass through. Selectors are left alone: a language
|
|
93
|
+
// selector's items intentionally target specific locales.
|
|
94
|
+
const localizePath = (path: string): string =>
|
|
95
|
+
path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
|
|
92
96
|
const tabs = options.navigation.tabs?.map((tab) => ({
|
|
93
97
|
...tab,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
98
|
+
items: tab.items?.map((item) => ({
|
|
99
|
+
...item,
|
|
100
|
+
path: localizePath(item.path),
|
|
101
|
+
})),
|
|
102
|
+
path: localizePath(tab.path),
|
|
97
103
|
}));
|
|
98
104
|
const real = pages.filter((page) => page.locale === code);
|
|
99
105
|
const localePages = localePagesFor(
|
|
@@ -109,6 +115,10 @@ const buildLocaleNavigation = (
|
|
|
109
115
|
display: options.navigation.sidebar.display,
|
|
110
116
|
featured: options.navigation.featured,
|
|
111
117
|
folderMeta: options.folderMeta,
|
|
118
|
+
// The localized tree root ("/" for the hidden default, "/fr" otherwise):
|
|
119
|
+
// the tab pointing here spans the whole tree and must not be treated as a
|
|
120
|
+
// tab section.
|
|
121
|
+
localizedRoot: localizeRoute("/", code, i18n),
|
|
112
122
|
// Meta files live in locale directories only under the `dir` parser
|
|
113
123
|
// (`fr/guides/meta.ts` -> key `fr/guides`). Under `dot`, translations sit
|
|
114
124
|
// next to the originals and `guides/meta.ts` applies to every locale —
|
package/src/core/i18n-ui.ts
CHANGED
|
@@ -19,16 +19,22 @@ const uiStringsObject = z.object({
|
|
|
19
19
|
connectMcp: z.string().default("Connect to MCP"),
|
|
20
20
|
copied: z.string().default("Copied!"),
|
|
21
21
|
copyClaudeCode: z.string().default("Copy Claude Code command"),
|
|
22
|
+
copyCode: z.string().default("Copy code"),
|
|
22
23
|
copyCodex: z.string().default("Copy Codex command"),
|
|
23
24
|
copyMarkdown: z.string().default("Copy as Markdown"),
|
|
24
25
|
copyServerUrl: z.string().default("Copy server URL"),
|
|
25
26
|
edit: z.string().default("Edit on GitHub"),
|
|
27
|
+
export: z.string().default("Export"),
|
|
28
|
+
exportEpub: z.string().default("Export to EPUB"),
|
|
29
|
+
exportPdf: z.string().default("Export to PDF"),
|
|
30
|
+
generating: z.string().default("Generating…"),
|
|
26
31
|
openInChat: z.string().default("Open in chat"),
|
|
27
32
|
scrollToTop: z.string().default("Scroll to top"),
|
|
28
33
|
})
|
|
29
34
|
.default({}),
|
|
30
35
|
ask: z
|
|
31
36
|
.object({
|
|
37
|
+
ai: z.string().default("AI"),
|
|
32
38
|
clear: z.string().default("Clear conversation"),
|
|
33
39
|
close: z.string().default("Close"),
|
|
34
40
|
copy: z.string().default("Copy conversation"),
|
|
@@ -39,6 +45,29 @@ const uiStringsObject = z.object({
|
|
|
39
45
|
send: z.string().default("Send"),
|
|
40
46
|
tip: z.string().default("Tip: You can open and close chat with"),
|
|
41
47
|
title: z.string().default("Ask AI"),
|
|
48
|
+
you: z.string().default("You"),
|
|
49
|
+
})
|
|
50
|
+
.default({}),
|
|
51
|
+
banner: z
|
|
52
|
+
.object({
|
|
53
|
+
dismiss: z.string().default("Dismiss announcement"),
|
|
54
|
+
})
|
|
55
|
+
.default({}),
|
|
56
|
+
changelog: z
|
|
57
|
+
.object({
|
|
58
|
+
description: z
|
|
59
|
+
.string()
|
|
60
|
+
.default(
|
|
61
|
+
"Product updates, new features, and fixes from every release."
|
|
62
|
+
),
|
|
63
|
+
// `{version}` is replaced with the major line ("2.x") at render time.
|
|
64
|
+
showReleases: z.string().default("Show {version} releases"),
|
|
65
|
+
title: z.string().default("Changelog"),
|
|
66
|
+
})
|
|
67
|
+
.default({}),
|
|
68
|
+
content: z
|
|
69
|
+
.object({
|
|
70
|
+
diagramError: z.string().default("Could not render this diagram."),
|
|
42
71
|
})
|
|
43
72
|
.default({}),
|
|
44
73
|
feedback: z
|
|
@@ -55,6 +84,21 @@ const uiStringsObject = z.object({
|
|
|
55
84
|
untranslated: z.string().default("Not translated"),
|
|
56
85
|
})
|
|
57
86
|
.default({}),
|
|
87
|
+
nav: z
|
|
88
|
+
.object({
|
|
89
|
+
back: z.string().default("Back"),
|
|
90
|
+
breadcrumb: z.string().default("Breadcrumb"),
|
|
91
|
+
closeNavigation: z.string().default("Close navigation"),
|
|
92
|
+
deprecated: z.string().default("deprecated"),
|
|
93
|
+
featured: z.string().default("Featured"),
|
|
94
|
+
githubRepository: z.string().default("GitHub repository"),
|
|
95
|
+
navigation: z.string().default("Navigation"),
|
|
96
|
+
primary: z.string().default("Primary"),
|
|
97
|
+
sections: z.string().default("Sections"),
|
|
98
|
+
toggleNavigation: z.string().default("Toggle navigation"),
|
|
99
|
+
toggleTheme: z.string().default("Toggle color theme"),
|
|
100
|
+
})
|
|
101
|
+
.default({}),
|
|
58
102
|
notFound: z
|
|
59
103
|
.object({
|
|
60
104
|
description: z
|
|
@@ -68,20 +112,30 @@ const uiStringsObject = z.object({
|
|
|
68
112
|
.object({
|
|
69
113
|
lastUpdated: z.string().default("Last updated on"),
|
|
70
114
|
next: z.string().default("Next"),
|
|
115
|
+
pagination: z.string().default("Pagination"),
|
|
71
116
|
previous: z.string().default("Previous"),
|
|
72
117
|
skipToContent: z.string().default("Skip to content"),
|
|
73
118
|
})
|
|
74
119
|
.default({}),
|
|
75
120
|
search: z
|
|
76
121
|
.object({
|
|
122
|
+
all: z.string().default("All"),
|
|
77
123
|
allLanguages: z.string().default("All languages"),
|
|
124
|
+
askAi: z.string().default("Ask AI"),
|
|
125
|
+
askAiHint: z.string().default("Get an instant answer from AI"),
|
|
78
126
|
button: z.string().default("Search"),
|
|
79
127
|
devOnly: z
|
|
80
128
|
.string()
|
|
81
129
|
.default("Search is available in the production build."),
|
|
130
|
+
error: z.string().default("Something went wrong. Please try again."),
|
|
82
131
|
label: z.string().default("Search docs"),
|
|
132
|
+
navigate: z.string().default("navigate"),
|
|
83
133
|
noResults: z.string().default("No results found."),
|
|
134
|
+
open: z.string().default("open"),
|
|
84
135
|
placeholder: z.string().default("Search documentation…"),
|
|
136
|
+
popular: z.string().default("Popular"),
|
|
137
|
+
preview: z.string().default("preview"),
|
|
138
|
+
results: z.string().default("Results"),
|
|
85
139
|
})
|
|
86
140
|
.default({}),
|
|
87
141
|
toc: z
|