blume 1.2.0 → 1.3.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 +45 -0
- package/dist/cli/index.js +1715 -539
- package/dist/cli/index.js.map +40 -31
- package/dist/types/core/config-input.d.ts +131 -11
- package/dist/types/core/config.d.ts +9 -1
- package/dist/types/core/data.d.ts +24 -5
- package/dist/types/core/i18n-ui.d.ts +58 -799
- package/dist/types/core/schema.d.ts +534 -3305
- package/dist/types/theme/fonts.d.ts +55 -11
- package/docs/02-deployment.mdx +2 -0
- package/docs/07-faq.mdx +14 -14
- package/docs/advanced/skills.mdx +2 -2
- package/docs/configuration/ai.mdx +126 -2
- package/docs/configuration/index.mdx +19 -1
- package/docs/configuration/search.mdx +17 -0
- package/docs/configuration/seo.mdx +26 -3
- package/docs/configuration/theming.mdx +44 -2
- package/docs/content/syntax.mdx +18 -2
- package/docs/reference/cli.mdx +3 -3
- package/package.json +9 -8
- package/skills/blume/SKILL.md +6 -4
- package/skills/blume-migrate/SKILL.md +5 -3
- package/skills/blume-migrate/references/mintlify.md +5 -5
- package/skills/blume-migrate/references/monorepo.md +2 -1
- package/src/ai/agent-readability.ts +31 -1
- package/src/ai/api-catalog.ts +81 -0
- package/src/ai/ask-context.ts +7 -1
- package/src/ai/ask-data.ts +1 -0
- package/src/ai/link-headers.ts +52 -0
- package/src/ai/llms.ts +12 -1
- package/src/ai/markdown.ts +15 -2
- package/src/ai/mcp/data.ts +7 -0
- package/src/ai/mcp/discovery.ts +70 -15
- package/src/ai/mcp/server.ts +14 -8
- package/src/ai/mcp/stdio.ts +4 -1
- package/src/ai/skills.ts +193 -0
- package/src/ai/tar.ts +104 -0
- package/src/ai/web-bot-auth.ts +30 -0
- package/src/astro/generate.ts +116 -6
- package/src/astro/integration.ts +52 -14
- package/src/astro/templates.ts +191 -37
- package/src/audit/catalog.ts +20 -0
- package/src/audit/checks/dns-aid.ts +190 -0
- package/src/audit/report.ts +5 -0
- package/src/audit/run.ts +2 -0
- package/src/cli/commands/build.ts +178 -9
- package/src/cli/init/scaffold.ts +1 -1
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/webmcp.ts +203 -0
- package/src/components/layout/NavTree.astro +4 -4
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +2 -0
- package/src/components/layout/RootLayout.astro +63 -11
- package/src/components/layout/Search.astro +2 -2
- package/src/components/layout/WebMcp.astro +49 -0
- package/src/components/layout/search/orama.ts +5 -2
- package/src/core/config-input.ts +143 -11
- package/src/core/config.ts +17 -1
- package/src/core/content-assets.ts +199 -0
- package/src/core/data.ts +21 -5
- package/src/core/diagnostics.ts +6 -5
- package/src/core/i18n-ui.ts +19 -28
- package/src/core/project-graph.ts +6 -0
- package/src/core/schema.ts +224 -71
- package/src/core/sources/normalize.ts +5 -5
- package/src/deploy/headers.ts +45 -3
- package/src/deploy/vercel-negotiation.ts +233 -0
- package/src/markdown/mermaid.ts +7 -1
- package/src/markdown/table-wrap.ts +33 -1
- package/src/og/card.ts +91 -22
- package/src/og/derive.ts +200 -0
- package/src/og/index.ts +6 -1
- package/src/search/orama-index.ts +151 -7
- package/src/theme/entry.ts +34 -13
- package/src/theme/fonts.ts +183 -30
- package/dist/types/og/card.d.ts +0 -63
- package/dist/types/og/dimensions.d.ts +0 -12
package/src/core/i18n-ui.ts
CHANGED
|
@@ -31,7 +31,7 @@ const uiStringsObject = z.object({
|
|
|
31
31
|
openInChat: z.string().default("Open in chat"),
|
|
32
32
|
scrollToTop: z.string().default("Scroll to top"),
|
|
33
33
|
})
|
|
34
|
-
.
|
|
34
|
+
.prefault({}),
|
|
35
35
|
ask: z
|
|
36
36
|
.object({
|
|
37
37
|
ai: z.string().default("AI"),
|
|
@@ -47,12 +47,12 @@ const uiStringsObject = z.object({
|
|
|
47
47
|
title: z.string().default("Ask AI"),
|
|
48
48
|
you: z.string().default("You"),
|
|
49
49
|
})
|
|
50
|
-
.
|
|
50
|
+
.prefault({}),
|
|
51
51
|
banner: z
|
|
52
52
|
.object({
|
|
53
53
|
dismiss: z.string().default("Dismiss announcement"),
|
|
54
54
|
})
|
|
55
|
-
.
|
|
55
|
+
.prefault({}),
|
|
56
56
|
changelog: z
|
|
57
57
|
.object({
|
|
58
58
|
description: z
|
|
@@ -64,12 +64,12 @@ const uiStringsObject = z.object({
|
|
|
64
64
|
showReleases: z.string().default("Show {version} releases"),
|
|
65
65
|
title: z.string().default("Changelog"),
|
|
66
66
|
})
|
|
67
|
-
.
|
|
67
|
+
.prefault({}),
|
|
68
68
|
content: z
|
|
69
69
|
.object({
|
|
70
70
|
diagramError: z.string().default("Could not render this diagram."),
|
|
71
71
|
})
|
|
72
|
-
.
|
|
72
|
+
.prefault({}),
|
|
73
73
|
feedback: z
|
|
74
74
|
.object({
|
|
75
75
|
no: z.string().default("No"),
|
|
@@ -77,13 +77,13 @@ const uiStringsObject = z.object({
|
|
|
77
77
|
thanks: z.string().default("Thanks for your feedback!"),
|
|
78
78
|
yes: z.string().default("Yes"),
|
|
79
79
|
})
|
|
80
|
-
.
|
|
80
|
+
.prefault({}),
|
|
81
81
|
languageSwitcher: z
|
|
82
82
|
.object({
|
|
83
83
|
label: z.string().default("Language"),
|
|
84
84
|
untranslated: z.string().default("Not translated"),
|
|
85
85
|
})
|
|
86
|
-
.
|
|
86
|
+
.prefault({}),
|
|
87
87
|
nav: z
|
|
88
88
|
.object({
|
|
89
89
|
back: z.string().default("Back"),
|
|
@@ -98,7 +98,7 @@ const uiStringsObject = z.object({
|
|
|
98
98
|
toggleNavigation: z.string().default("Toggle navigation"),
|
|
99
99
|
toggleTheme: z.string().default("Toggle color theme"),
|
|
100
100
|
})
|
|
101
|
-
.
|
|
101
|
+
.prefault({}),
|
|
102
102
|
notFound: z
|
|
103
103
|
.object({
|
|
104
104
|
description: z
|
|
@@ -107,7 +107,7 @@ const uiStringsObject = z.object({
|
|
|
107
107
|
home: z.string().default("Back to home"),
|
|
108
108
|
title: z.string().default("Page not found"),
|
|
109
109
|
})
|
|
110
|
-
.
|
|
110
|
+
.prefault({}),
|
|
111
111
|
page: z
|
|
112
112
|
.object({
|
|
113
113
|
lastUpdated: z.string().default("Last updated on"),
|
|
@@ -116,7 +116,7 @@ const uiStringsObject = z.object({
|
|
|
116
116
|
previous: z.string().default("Previous"),
|
|
117
117
|
skipToContent: z.string().default("Skip to content"),
|
|
118
118
|
})
|
|
119
|
-
.
|
|
119
|
+
.prefault({}),
|
|
120
120
|
search: z
|
|
121
121
|
.object({
|
|
122
122
|
all: z.string().default("All"),
|
|
@@ -137,36 +137,27 @@ const uiStringsObject = z.object({
|
|
|
137
137
|
preview: z.string().default("preview"),
|
|
138
138
|
results: z.string().default("Results"),
|
|
139
139
|
})
|
|
140
|
-
.
|
|
140
|
+
.prefault({}),
|
|
141
141
|
toc: z
|
|
142
142
|
.object({
|
|
143
143
|
title: z.string().default("On this page"),
|
|
144
144
|
})
|
|
145
|
-
.
|
|
145
|
+
.prefault({}),
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
export const uiStringsSchema = uiStringsObject.
|
|
148
|
+
export const uiStringsSchema = uiStringsObject.prefault({});
|
|
149
149
|
|
|
150
150
|
/** A fully-resolved dictionary; every key present. */
|
|
151
151
|
export type UIStrings = z.infer<typeof uiStringsObject>;
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
|
-
* The English baseline, derived from the schema defaults.
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* default value without re-parsing it through the inner type, so a bare
|
|
160
|
-
* `parse({})` leaves every group undefined and collapses each to `{}` there.
|
|
161
|
-
* Because these components resolve Zod from the consuming project — which may be
|
|
162
|
-
* on v4 while the CLI bundles v3 — the runtime baseline would silently render
|
|
163
|
-
* blank (empty search labels, aria-labels, skip link). Naming every group keeps
|
|
164
|
-
* the baseline fully populated on both Zod 3 and 4.
|
|
154
|
+
* The English baseline, derived from the schema defaults. The groups use
|
|
155
|
+
* `.prefault({})` (not `.default({})`) so an absent group is still parsed
|
|
156
|
+
* through its inner type and every field default applies — under Zod 4's
|
|
157
|
+
* `.default()` semantics a bare `parse({})` would collapse each group to a
|
|
158
|
+
* literal `{}` and the runtime would silently render blank chrome.
|
|
165
159
|
*/
|
|
166
|
-
const
|
|
167
|
-
Object.keys(uiStringsObject.shape).map((group) => [group, {}])
|
|
168
|
-
);
|
|
169
|
-
export const EN_UI: UIStrings = uiStringsObject.parse(EN_UI_INPUT);
|
|
160
|
+
export const EN_UI: UIStrings = uiStringsObject.parse({});
|
|
170
161
|
|
|
171
162
|
/**
|
|
172
163
|
* A partial override: `{ group: { key: "translation" } }`. Validated loosely
|
|
@@ -75,6 +75,11 @@ export interface BlumeProject {
|
|
|
75
75
|
droppedPages: number;
|
|
76
76
|
/** The instantiated content sources, for lazy entry reads (search/AI/raw). */
|
|
77
77
|
sources: ContentSource[];
|
|
78
|
+
/**
|
|
79
|
+
* Whether the config file set `theme.fonts` itself (see
|
|
80
|
+
* {@link ConfigLoadResult.themeFontsConfigured}); gates OG font derivation.
|
|
81
|
+
*/
|
|
82
|
+
themeFontsConfigured: boolean;
|
|
78
83
|
/**
|
|
79
84
|
* Discovered `examples/` sources keyed by `<Component path>`, attached by the
|
|
80
85
|
* runtime/eject layer after {@link scanProject} (example discovery is an Astro
|
|
@@ -306,5 +311,6 @@ export const scanProject = async (
|
|
|
306
311
|
manifest,
|
|
307
312
|
mode,
|
|
308
313
|
sources,
|
|
314
|
+
themeFontsConfigured: configResult.themeFontsConfigured,
|
|
309
315
|
};
|
|
310
316
|
};
|
package/src/core/schema.ts
CHANGED
|
@@ -61,10 +61,7 @@ const sidebarMetaSchema = z.strictObject({
|
|
|
61
61
|
const xHandleSchema = z.string().transform(normalizeXHandle).optional();
|
|
62
62
|
|
|
63
63
|
const seoMetaSchema = z.strictObject({
|
|
64
|
-
|
|
65
|
-
// schemas must stay dual-compatible with consumer projects on Zod 4.
|
|
66
|
-
// oxlint-disable-next-line react-doctor/zod-v4-prefer-top-level-string-formats
|
|
67
|
-
canonical: z.string().url().optional(),
|
|
64
|
+
canonical: z.url().optional(),
|
|
68
65
|
description: z.string().optional(),
|
|
69
66
|
image: z.string().optional(),
|
|
70
67
|
noindex: z.boolean().default(false),
|
|
@@ -109,9 +106,14 @@ const authorSchema = z.union([
|
|
|
109
106
|
.catchall(z.unknown()),
|
|
110
107
|
]);
|
|
111
108
|
|
|
109
|
+
// Shorthand defaults use `.prefault()`, not `.default()`, wherever the value
|
|
110
|
+
// must still be parsed — Zod 4's `.default()` returns the value as-is, so a
|
|
111
|
+
// `.default({})` on an object with inner defaults (or a transform) would
|
|
112
|
+
// resolve to a bare `{}` instead of the fully-defaulted shape.
|
|
113
|
+
|
|
112
114
|
/** Frontmatter accepted on any content page. */
|
|
113
115
|
const pageMetaBaseSchema = z.strictObject({
|
|
114
|
-
ai: aiMetaSchema.
|
|
116
|
+
ai: aiMetaSchema.prefault({}),
|
|
115
117
|
/** Post author(s) for blog/changelog content; preserved, not yet rendered. */
|
|
116
118
|
authors: z.union([authorSchema, z.array(authorSchema)]).optional(),
|
|
117
119
|
changelog: changelogMetaSchema.optional(),
|
|
@@ -125,9 +127,9 @@ const pageMetaBaseSchema = z.strictObject({
|
|
|
125
127
|
/** Overrides the git-derived last-modified date when `lastModified` is on. */
|
|
126
128
|
lastModified: dateSchema.optional(),
|
|
127
129
|
noindex: z.boolean().default(false),
|
|
128
|
-
search: searchMetaSchema.
|
|
129
|
-
seo: seoMetaSchema.
|
|
130
|
-
sidebar: sidebarMetaSchema.
|
|
130
|
+
search: searchMetaSchema.prefault({}),
|
|
131
|
+
seo: seoMetaSchema.prefault({}),
|
|
132
|
+
sidebar: sidebarMetaSchema.prefault({}),
|
|
131
133
|
slug: z.string().optional(),
|
|
132
134
|
title: z.string().optional(),
|
|
133
135
|
// No default: an absent `type` must fall through to `content.defaultType`.
|
|
@@ -410,33 +412,79 @@ export type SidebarItemConfig =
|
|
|
410
412
|
root?: string;
|
|
411
413
|
};
|
|
412
414
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
415
|
+
// Zod 4's `ZodType` defaults its Input parameter to `unknown` (it no longer
|
|
416
|
+
// mirrors Output), so the recursive annotation names both — otherwise
|
|
417
|
+
// `z.input` of anything containing this schema degrades to `unknown`.
|
|
418
|
+
const sidebarItemSchema: z.ZodType<SidebarItemConfig, SidebarItemConfig> =
|
|
419
|
+
z.lazy(() =>
|
|
420
|
+
z.union([
|
|
421
|
+
z.string(),
|
|
422
|
+
z.strictObject({
|
|
423
|
+
badge: z.string().optional(),
|
|
424
|
+
collapsed: z.boolean().optional(),
|
|
425
|
+
directory: directoryModeSchema.optional(),
|
|
426
|
+
display: sidebarDisplaySchema.optional(),
|
|
427
|
+
href: z.string().optional(),
|
|
428
|
+
icon: iconName.optional(),
|
|
429
|
+
items: z.array(sidebarItemSchema).optional(),
|
|
430
|
+
label: z.string(),
|
|
431
|
+
root: z.string().optional(),
|
|
432
|
+
}),
|
|
433
|
+
])
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const fontFallbackSchema = z.enum(["sans", "serif", "mono"]);
|
|
437
|
+
|
|
438
|
+
/** Any family from a zero-config Astro provider, by name. */
|
|
439
|
+
const remoteFontSchema = z.strictObject({
|
|
440
|
+
fallback: fontFallbackSchema.optional(),
|
|
441
|
+
name: z.string().min(1),
|
|
442
|
+
provider: z
|
|
443
|
+
.enum(["google", "fontsource", "bunny", "fontshare"])
|
|
444
|
+
.default("google"),
|
|
445
|
+
weights: z
|
|
446
|
+
.array(
|
|
447
|
+
z.union([z.number().int().positive(), z.string().regex(/^\d+\.\.\d+$/u)])
|
|
448
|
+
)
|
|
449
|
+
.nonempty()
|
|
450
|
+
.optional(),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
/** One local `@font-face`: a file plus optional weight/style (else inferred). */
|
|
454
|
+
const localFontVariantSchema = z.strictObject({
|
|
455
|
+
src: z.string().min(1),
|
|
456
|
+
style: z.enum(["normal", "italic", "oblique"]).optional(),
|
|
457
|
+
weight: z
|
|
458
|
+
.union([
|
|
459
|
+
z.number().int().positive(),
|
|
460
|
+
z.string().regex(/^\d+(?:\.\.\d+)?$/u),
|
|
461
|
+
])
|
|
462
|
+
.optional(),
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
/** A self-hosted family loaded from font files in the project. */
|
|
466
|
+
const localFontSchema = z.strictObject({
|
|
467
|
+
fallback: fontFallbackSchema.optional(),
|
|
468
|
+
name: z.string().min(1),
|
|
469
|
+
variants: z.array(localFontVariantSchema).nonempty(),
|
|
438
470
|
});
|
|
439
471
|
|
|
472
|
+
/**
|
|
473
|
+
* A role's font: a curated Google Font slug (see `theme/fonts.ts`), a
|
|
474
|
+
* remote-provider family, or local font files. Bare strings must be curated
|
|
475
|
+
* slugs so a typo fails with the supported list instead of a provider error.
|
|
476
|
+
*/
|
|
477
|
+
const fontValueSchema = z
|
|
478
|
+
.union([z.string(), remoteFontSchema, localFontSchema])
|
|
479
|
+
.superRefine((value, ctx) => {
|
|
480
|
+
if (typeof value === "string" && !isFontSlug(value)) {
|
|
481
|
+
ctx.addIssue({
|
|
482
|
+
code: z.ZodIssueCode.custom,
|
|
483
|
+
message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}. For any other family, use the object form: { name: "..." } (remote provider) or { name: "...", variants: [...] } (local files).`,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
440
488
|
/**
|
|
441
489
|
* An optional per-mode theme value: a string applies to both color modes; a
|
|
442
490
|
* `{ light, dark }` object sets each mode individually (either may be
|
|
@@ -470,11 +518,11 @@ const themeConfigSchema = z.strictObject({
|
|
|
470
518
|
backgroundImage: perModeValueSchema,
|
|
471
519
|
fonts: z
|
|
472
520
|
.strictObject({
|
|
473
|
-
body:
|
|
474
|
-
display:
|
|
475
|
-
mono:
|
|
521
|
+
body: fontValueSchema.default("inter"),
|
|
522
|
+
display: fontValueSchema.default("inter-tight"),
|
|
523
|
+
mono: fontValueSchema.default("ibm-plex-mono"),
|
|
476
524
|
})
|
|
477
|
-
.
|
|
525
|
+
.prefault({}),
|
|
478
526
|
layout: z.enum(["sidebar"]).default("sidebar"),
|
|
479
527
|
mode: z.enum(["system", "light", "dark"]).default("system"),
|
|
480
528
|
radius: z.enum(["none", "sm", "md", "lg"]).default("md"),
|
|
@@ -542,7 +590,7 @@ const searchConfigSchema = z
|
|
|
542
590
|
.strictObject({
|
|
543
591
|
includeHiddenPages: z.boolean().default(false),
|
|
544
592
|
})
|
|
545
|
-
.
|
|
593
|
+
.prefault({}),
|
|
546
594
|
mixedbread: mixedbreadSearchSchema.optional(),
|
|
547
595
|
oramaCloud: oramaCloudSearchSchema.optional(),
|
|
548
596
|
/** Curated links for the Cmd+K empty state; defaults to the first sidebar pages. */
|
|
@@ -573,6 +621,37 @@ export const askAiProviders = [
|
|
|
573
621
|
"openai-compatible",
|
|
574
622
|
] as const;
|
|
575
623
|
|
|
624
|
+
/**
|
|
625
|
+
* JWK parameters that carry private or secret key material (RFC 7518): the
|
|
626
|
+
* private exponent/scalar, the RSA CRT parameters, and the symmetric key.
|
|
627
|
+
* A directory is public by definition, so any of these in a configured key is
|
|
628
|
+
* a leaked credential, not a config style choice — reject loudly.
|
|
629
|
+
*/
|
|
630
|
+
const PRIVATE_JWK_PARAMS = ["d", "p", "q", "dp", "dq", "qi", "oth", "k"];
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* A public JWK for the Web Bot Auth signature directory. Shape is left to the
|
|
634
|
+
* signing setup (Ed25519 `OKP` keys in current deployments) — validation only
|
|
635
|
+
* requires the mandatory `kty` and refuses private key material.
|
|
636
|
+
*/
|
|
637
|
+
const publicJwkSchema = z
|
|
638
|
+
.record(z.string(), z.unknown())
|
|
639
|
+
.superRefine((jwk, ctx) => {
|
|
640
|
+
if (typeof jwk.kty !== "string" || jwk.kty.length === 0) {
|
|
641
|
+
ctx.addIssue({
|
|
642
|
+
code: z.ZodIssueCode.custom,
|
|
643
|
+
message: 'A JWK must declare its key type ("kty").',
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
const leaked = PRIVATE_JWK_PARAMS.filter((param) => param in jwk);
|
|
647
|
+
if (leaked.length > 0) {
|
|
648
|
+
ctx.addIssue({
|
|
649
|
+
code: z.ZodIssueCode.custom,
|
|
650
|
+
message: `The JWK contains private key material ("${leaked.join('", "')}") — the signatures directory is public, so list only public keys and keep the private key where the signing agent runs.`,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
576
655
|
const mcpConfigSchema = z.strictObject({
|
|
577
656
|
enabled: z.boolean().default(false),
|
|
578
657
|
/** Optional system hint passed to connecting agents. */
|
|
@@ -616,9 +695,7 @@ const aiConfigSchema = z.strictObject({
|
|
|
616
695
|
apiKeyEnv: z.string().optional(),
|
|
617
696
|
// Base URL of the backend. Required for `openai-compatible` only when no
|
|
618
697
|
// external endpoint is supplied; for named providers it overrides the preset.
|
|
619
|
-
|
|
620
|
-
// oxlint-disable-next-line react-doctor/zod-v4-prefer-top-level-string-formats
|
|
621
|
-
baseUrl: z.string().url().optional(),
|
|
698
|
+
baseUrl: z.url().optional(),
|
|
622
699
|
enabled: z.boolean().default(false),
|
|
623
700
|
// Optional external endpoint for projects that keep their docs static
|
|
624
701
|
// and host Ask AI in an existing backend. Absolute URLs and root-relative
|
|
@@ -686,7 +763,35 @@ const aiConfigSchema = z.strictObject({
|
|
|
686
763
|
)
|
|
687
764
|
.default({}),
|
|
688
765
|
/** Expose the docs as an MCP server for connecting agents. */
|
|
689
|
-
mcp: mcpConfigSchema.
|
|
766
|
+
mcp: mcpConfigSchema.prefault({}),
|
|
767
|
+
/**
|
|
768
|
+
* Publish Agent Skills for discovery: a directory (resolved against the
|
|
769
|
+
* project root) whose subdirectories each hold a `SKILL.md`. The build
|
|
770
|
+
* copies each skill under `/.well-known/agent-skills/` — a lone `SKILL.md`
|
|
771
|
+
* verbatim, a skill with supporting files as a `.tar.gz` — and emits the
|
|
772
|
+
* discovery index (`index.json`) with SHA-256 digests per the Agent Skills
|
|
773
|
+
* Discovery RFC.
|
|
774
|
+
*/
|
|
775
|
+
skills: z.string().min(1).optional(),
|
|
776
|
+
/**
|
|
777
|
+
* Web Bot Auth (IETF `webbotauth`): publish the org's HTTP Message
|
|
778
|
+
* Signature public keys at `/.well-known/http-message-signatures-directory`
|
|
779
|
+
* so sites receiving requests from the org's agents can verify them.
|
|
780
|
+
* Opt-in and public-keys-only — the private keys live wherever the signing
|
|
781
|
+
* agents run, never in the site.
|
|
782
|
+
*/
|
|
783
|
+
webBotAuth: z
|
|
784
|
+
.strictObject({
|
|
785
|
+
keys: z.array(publicJwkSchema).default([]),
|
|
786
|
+
})
|
|
787
|
+
.prefault({}),
|
|
788
|
+
/**
|
|
789
|
+
* WebMCP: register in-page tools (search, page Markdown, the docs index)
|
|
790
|
+
* on the browser's model context so agentic browsers can drive the docs
|
|
791
|
+
* without a separate MCP connection. A tiny script that no-ops in browsers
|
|
792
|
+
* without the API; on by default.
|
|
793
|
+
*/
|
|
794
|
+
webmcp: z.boolean().default(true),
|
|
690
795
|
});
|
|
691
796
|
|
|
692
797
|
/**
|
|
@@ -720,7 +825,7 @@ const navigationConfigSchema = z.strictObject({
|
|
|
720
825
|
items: z.array(sidebarItemSchema).optional(),
|
|
721
826
|
}),
|
|
722
827
|
])
|
|
723
|
-
.
|
|
828
|
+
.prefault({})
|
|
724
829
|
.transform((value) =>
|
|
725
830
|
Array.isArray(value) ? { display: "flat" as const, items: value } : value
|
|
726
831
|
),
|
|
@@ -828,9 +933,7 @@ const deploymentConfigSchema = z.strictObject({
|
|
|
828
933
|
.default(null),
|
|
829
934
|
base: z.string().optional(),
|
|
830
935
|
output: z.enum(["static", "server"]).default("static"),
|
|
831
|
-
|
|
832
|
-
// oxlint-disable-next-line react-doctor/zod-v4-prefer-top-level-string-formats
|
|
833
|
-
site: z.string().url().optional(),
|
|
936
|
+
site: z.url().optional(),
|
|
834
937
|
});
|
|
835
938
|
|
|
836
939
|
const redirectSchema = z.strictObject({
|
|
@@ -841,6 +944,33 @@ const redirectSchema = z.strictObject({
|
|
|
841
944
|
to: z.string(),
|
|
842
945
|
});
|
|
843
946
|
|
|
947
|
+
/**
|
|
948
|
+
* One authorized remote image source, passed through to Astro's
|
|
949
|
+
* `image.remotePatterns`. Wildcards follow Astro's rules: `hostname` accepts
|
|
950
|
+
* `**.example.com` (any depth) or `*.example.com` (one level), `pathname`
|
|
951
|
+
* accepts `/dir/**` or `/dir/*` the same way.
|
|
952
|
+
*/
|
|
953
|
+
const imageRemotePatternSchema = z.strictObject({
|
|
954
|
+
hostname: z.string().optional(),
|
|
955
|
+
pathname: z.string().optional(),
|
|
956
|
+
port: z.string().optional(),
|
|
957
|
+
protocol: z.string().optional(),
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Image optimization (`image`). Local images referenced by relative path
|
|
962
|
+
* (``) are optimized at build time automatically —
|
|
963
|
+
* compressed, converted to WebP, and stamped with intrinsic dimensions.
|
|
964
|
+
* Remote images are only optimized when their host is authorized here;
|
|
965
|
+
* both fields map directly onto Astro's `image` config.
|
|
966
|
+
*/
|
|
967
|
+
const imageConfigSchema = z.strictObject({
|
|
968
|
+
/** Hosts whose remote images may be optimized, e.g. `["cdn.example.com"]`. */
|
|
969
|
+
domains: z.array(z.string()).default([]),
|
|
970
|
+
/** Pattern-based host authorization, for wildcards `domains` can't express. */
|
|
971
|
+
remotePatterns: z.array(imageRemotePatternSchema).default([]),
|
|
972
|
+
});
|
|
973
|
+
|
|
844
974
|
/**
|
|
845
975
|
* X (Twitter) attribution. The account fields feed `twitter:site` (the site's
|
|
846
976
|
* account) and `twitter:creator` (the author's), which is the one piece of X
|
|
@@ -872,10 +1002,11 @@ const ogPaletteSchema = z.strictObject({
|
|
|
872
1002
|
});
|
|
873
1003
|
|
|
874
1004
|
/**
|
|
875
|
-
* A
|
|
876
|
-
* family name; the object form pins the weight (a number, a list, or
|
|
877
|
-
* range like `"100..900"`) and style
|
|
878
|
-
*
|
|
1005
|
+
* A font to load into the OG card renderer. A bare string is a Google Fonts
|
|
1006
|
+
* family name; the name-only object form pins the weight (a number, a list, or
|
|
1007
|
+
* a variable range like `"100..900"`) and style, fetched from Google Fonts at
|
|
1008
|
+
* build. The `src` form reads a local font file from the project instead.
|
|
1009
|
+
* Either way Takumi does per-glyph fallback, so a family covering a script
|
|
879
1010
|
* (e.g. Noto Sans JP for CJK) fixes tofu without touching how Latin renders.
|
|
880
1011
|
*/
|
|
881
1012
|
const ogFontWeightSchema = z.union([
|
|
@@ -891,9 +1022,21 @@ const ogFontSchema = z.union([
|
|
|
891
1022
|
style: z.union([ogFontStyleSchema, z.array(ogFontStyleSchema)]).optional(),
|
|
892
1023
|
weight: ogFontWeightSchema.optional(),
|
|
893
1024
|
}),
|
|
1025
|
+
/** A local font file, read from the project at build. */
|
|
1026
|
+
z.strictObject({
|
|
1027
|
+
name: z.string(),
|
|
1028
|
+
src: z.string().min(1),
|
|
1029
|
+
style: ogFontStyleSchema.optional(),
|
|
1030
|
+
weight: z.number().int().positive().optional(),
|
|
1031
|
+
}),
|
|
894
1032
|
]);
|
|
895
1033
|
|
|
896
1034
|
const ogConfigSchema = z.strictObject({
|
|
1035
|
+
/**
|
|
1036
|
+
* Card subtitle. Defaults to the site description; a string overrides it,
|
|
1037
|
+
* `false` renders the card without one.
|
|
1038
|
+
*/
|
|
1039
|
+
description: z.union([z.string(), z.literal(false)]).optional(),
|
|
897
1040
|
/**
|
|
898
1041
|
* Generate a per-page Open Graph image. Defaults to on once a deployment
|
|
899
1042
|
* site URL is known (set or auto-detected) and off otherwise, since
|
|
@@ -907,10 +1050,19 @@ const ogConfigSchema = z.strictObject({
|
|
|
907
1050
|
* Fetched from Google Fonts at build.
|
|
908
1051
|
*/
|
|
909
1052
|
fonts: z.array(ogFontSchema).optional(),
|
|
910
|
-
/**
|
|
911
|
-
|
|
1053
|
+
/**
|
|
1054
|
+
* Local SVG used in the generated card instead of the site logo; `false`
|
|
1055
|
+
* renders the card without any brand mark.
|
|
1056
|
+
*/
|
|
1057
|
+
logo: z.union([z.string(), z.literal(false)]).optional(),
|
|
912
1058
|
/** Optional generated-card colors. */
|
|
913
1059
|
palette: ogPaletteSchema.optional(),
|
|
1060
|
+
/**
|
|
1061
|
+
* Footer site text. Defaults to the deployment site's host plus
|
|
1062
|
+
* `deployment.base` (`docs.acme.com`, `user.github.io/repo`); a string
|
|
1063
|
+
* overrides it, `false` hides it.
|
|
1064
|
+
*/
|
|
1065
|
+
site: z.union([z.string(), z.literal(false)]).optional(),
|
|
914
1066
|
/**
|
|
915
1067
|
* Card headlines for custom `.astro` pages, keyed by route (`"/"`, `"/cli"`).
|
|
916
1068
|
* A custom page has no frontmatter to read, so its card is otherwise titled
|
|
@@ -970,11 +1122,11 @@ const seoConfigSchema = z.strictObject({
|
|
|
970
1122
|
*/
|
|
971
1123
|
agentReadability: z.boolean().default(true),
|
|
972
1124
|
/** robots.txt `Content-Signal` usage declaration (on by default). */
|
|
973
|
-
contentSignals: contentSignalsSchema.
|
|
1125
|
+
contentSignals: contentSignalsSchema.prefault(true),
|
|
974
1126
|
og: ogConfigSchema.default({}),
|
|
975
1127
|
/** Generate robots.txt (with a Sitemap reference when available). */
|
|
976
1128
|
robots: z.boolean().default(true),
|
|
977
|
-
rss: rssConfigSchema.
|
|
1129
|
+
rss: rssConfigSchema.prefault({}),
|
|
978
1130
|
/** Generate sitemap.xml (requires deployment.site). */
|
|
979
1131
|
sitemap: z.boolean().default(true),
|
|
980
1132
|
/** Emit schema.org JSON-LD in each page's <head>. */
|
|
@@ -1026,7 +1178,7 @@ const codeBlockThemeSchema = z.strictObject({
|
|
|
1026
1178
|
});
|
|
1027
1179
|
|
|
1028
1180
|
const codeBlocksConfigSchema = z.strictObject({
|
|
1029
|
-
theme: codeBlockThemeSchema.
|
|
1181
|
+
theme: codeBlockThemeSchema.prefault({}),
|
|
1030
1182
|
});
|
|
1031
1183
|
|
|
1032
1184
|
/**
|
|
@@ -1127,8 +1279,8 @@ const codeConfigSchema = z.strictObject({
|
|
|
1127
1279
|
|
|
1128
1280
|
const markdownConfigSchema = z.strictObject({
|
|
1129
1281
|
/** Code-block rendering: language icons and line wrapping. */
|
|
1130
|
-
code: codeConfigSchema.
|
|
1131
|
-
codeBlocks: codeBlocksConfigSchema.
|
|
1282
|
+
code: codeConfigSchema.prefault({}),
|
|
1283
|
+
codeBlocks: codeBlocksConfigSchema.prefault({}),
|
|
1132
1284
|
/**
|
|
1133
1285
|
* Wrap each `##`–`######` heading in a link to its own anchor so readers can
|
|
1134
1286
|
* click to copy, bookmark, or share a permalink to that section. On by
|
|
@@ -1293,9 +1445,9 @@ const tocConfigSchema = z
|
|
|
1293
1445
|
});
|
|
1294
1446
|
|
|
1295
1447
|
export const blumeConfigSchema = z.strictObject({
|
|
1296
|
-
ai: aiConfigSchema.
|
|
1448
|
+
ai: aiConfigSchema.prefault({}),
|
|
1297
1449
|
analytics: analyticsConfigSchema.optional(),
|
|
1298
|
-
asyncapi: asyncapiConfigSchema.
|
|
1450
|
+
asyncapi: asyncapiConfigSchema.prefault({}),
|
|
1299
1451
|
banner: bannerConfigSchema.optional(),
|
|
1300
1452
|
/**
|
|
1301
1453
|
* Site-wide mount point prepended to every generated route (e.g. `/docs`),
|
|
@@ -1307,13 +1459,13 @@ export const blumeConfigSchema = z.strictObject({
|
|
|
1307
1459
|
.string()
|
|
1308
1460
|
.optional()
|
|
1309
1461
|
.transform((value) => normalizeBasePath(value)),
|
|
1310
|
-
content: contentConfigSchema.
|
|
1462
|
+
content: contentConfigSchema.prefault({}),
|
|
1311
1463
|
/**
|
|
1312
1464
|
* Date presentation for the "last updated" stamp and the changelog timeline.
|
|
1313
1465
|
* Pass-through `Intl.DateTimeFormat` options; defaults to `{ dateStyle: "long" }`.
|
|
1314
1466
|
*/
|
|
1315
1467
|
dateFormat: dateFormatConfigSchema.default({ dateStyle: "long" }),
|
|
1316
|
-
deployment: deploymentConfigSchema.
|
|
1468
|
+
deployment: deploymentConfigSchema.prefault({}),
|
|
1317
1469
|
description: z.string().optional(),
|
|
1318
1470
|
/**
|
|
1319
1471
|
* Where `<Component path>` resolves live previews and their source from.
|
|
@@ -1322,24 +1474,25 @@ export const blumeConfigSchema = z.strictObject({
|
|
|
1322
1474
|
* files. The object form adds `css`: a stylesheet injected into every
|
|
1323
1475
|
* preview frame (design tokens, shadcn variables, `@theme` mappings).
|
|
1324
1476
|
*/
|
|
1325
|
-
examples: examplesConfigSchema.
|
|
1326
|
-
export: exportConfigSchema.
|
|
1477
|
+
examples: examplesConfigSchema.prefault("examples"),
|
|
1478
|
+
export: exportConfigSchema.prefault(false),
|
|
1327
1479
|
feedback: z.boolean().default(true),
|
|
1328
1480
|
/** Opt-in custom frontmatter keys, validated by user-supplied schemas. */
|
|
1329
|
-
frontmatter: frontmatterConfigSchema.
|
|
1481
|
+
frontmatter: frontmatterConfigSchema.prefault({}),
|
|
1330
1482
|
github: githubConfigSchema.optional(),
|
|
1331
1483
|
i18n: i18nConfigSchema.optional(),
|
|
1484
|
+
image: imageConfigSchema.prefault({}),
|
|
1332
1485
|
integrations: z.array(z.custom<AstroIntegration>()).default([]),
|
|
1333
1486
|
lastModified: lastModifiedConfigSchema.default(false),
|
|
1334
1487
|
logo: logoConfigSchema.optional(),
|
|
1335
|
-
markdown: markdownConfigSchema.
|
|
1336
|
-
navigation: navigationConfigSchema.
|
|
1337
|
-
openapi: openapiConfigSchema.
|
|
1338
|
-
react: reactConfigSchema.
|
|
1488
|
+
markdown: markdownConfigSchema.prefault({}),
|
|
1489
|
+
navigation: navigationConfigSchema.prefault({}),
|
|
1490
|
+
openapi: openapiConfigSchema.prefault({}),
|
|
1491
|
+
react: reactConfigSchema.prefault({}),
|
|
1339
1492
|
redirects: z.array(redirectSchema).default([]),
|
|
1340
|
-
search: searchConfigSchema.
|
|
1341
|
-
seo: seoConfigSchema.
|
|
1342
|
-
theme: themeConfigSchema.
|
|
1493
|
+
search: searchConfigSchema.prefault({}),
|
|
1494
|
+
seo: seoConfigSchema.prefault({}),
|
|
1495
|
+
theme: themeConfigSchema.prefault({}),
|
|
1343
1496
|
title: z.string().default("Documentation"),
|
|
1344
1497
|
toc: tocConfigSchema,
|
|
1345
1498
|
});
|
|
@@ -105,14 +105,14 @@ const mapRoute = (
|
|
|
105
105
|
const CODE_FENCE = /^(?<delimiter>```|~~~)/u;
|
|
106
106
|
|
|
107
107
|
/** The fence delimiter opening the current code block, or null outside one. */
|
|
108
|
-
type FenceState = "```" | "~~~" | null;
|
|
108
|
+
export type FenceState = "```" | "~~~" | null;
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* Advance the fenced-code state for one line: an opening fence records its
|
|
112
112
|
* delimiter, only the matching delimiter closes it, and any other line leaves
|
|
113
113
|
* the state untouched.
|
|
114
114
|
*/
|
|
115
|
-
const nextFenceState = (line: string, fence: FenceState): FenceState => {
|
|
115
|
+
export const nextFenceState = (line: string, fence: FenceState): FenceState => {
|
|
116
116
|
const trimmed = line.trimStart();
|
|
117
117
|
const delimiter = trimmed.match(CODE_FENCE)?.groups?.delimiter as
|
|
118
118
|
| Exclude<FenceState, null>
|
|
@@ -340,12 +340,12 @@ const MD_LINK =
|
|
|
340
340
|
/\[(?<label>(?:[^[\]]|\[[^\]]*\])*)\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
|
|
341
341
|
// An image inside a link label; its target was matched (and so validated) as a
|
|
342
342
|
// link of its own before labels admitted nesting, and still should be.
|
|
343
|
-
const MD_IMAGE =
|
|
343
|
+
export const MD_IMAGE =
|
|
344
344
|
/!\[[^\]]*\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
|
|
345
|
-
const INLINE_CODE = /`[^`]*`/gu;
|
|
345
|
+
export const INLINE_CODE = /`[^`]*`/gu;
|
|
346
346
|
|
|
347
347
|
/** Column (0-based, within `matched`) where a link/image match's target starts. */
|
|
348
|
-
const targetOffsetIn = (
|
|
348
|
+
export const targetOffsetIn = (
|
|
349
349
|
matched: string,
|
|
350
350
|
target: string,
|
|
351
351
|
title: string | undefined
|