blume 0.0.0 → 0.1.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.
Files changed (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,1057 @@
1
+ import { z } from "zod";
2
+
3
+ import { FONT_SLUGS, isFontSlug } from "../theme/fonts.ts";
4
+ import { uiLocaleOverridesSchema } from "./i18n-ui.ts";
5
+ import type { ContentSource } from "./sources/types.ts";
6
+
7
+ /**
8
+ * Public Blume schemas.
9
+ *
10
+ * These are exported from `blume/schema` so migration tools, editor
11
+ * integrations, and the runtime share a single source of validation truth.
12
+ */
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Shared primitives
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** Icon inputs in serializable contexts (frontmatter, meta files). */
19
+ const iconName = z.string().min(1);
20
+
21
+ const hydrationMode = z.enum(["load", "idle", "visible", "media", "only"]);
22
+ export type HydrationMode = z.infer<typeof hydrationMode>;
23
+
24
+ /**
25
+ * A publish date in frontmatter. YAML auto-parses an unquoted `2026-01-01` into
26
+ * a `Date`, so accept either form and normalize to an ISO string.
27
+ */
28
+ const dateSchema = z
29
+ .union([z.string(), z.date()])
30
+ .transform((value) => (value instanceof Date ? value.toISOString() : value));
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Page frontmatter
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const sidebarMetaSchema = z
37
+ .object({
38
+ badge: z.string().optional(),
39
+ hidden: z.boolean().default(false),
40
+ icon: iconName.optional(),
41
+ label: z.string().optional(),
42
+ order: z.number().optional(),
43
+ })
44
+ .strict();
45
+
46
+ const seoMetaSchema = z
47
+ .object({
48
+ canonical: z.string().url().optional(),
49
+ description: z.string().optional(),
50
+ image: z.string().optional(),
51
+ noindex: z.boolean().default(false),
52
+ title: z.string().optional(),
53
+ })
54
+ .strict();
55
+
56
+ const searchMetaSchema = z
57
+ .object({
58
+ boost: z.number().optional(),
59
+ exclude: z.boolean().default(false),
60
+ tags: z.array(z.string()).optional(),
61
+ })
62
+ .strict();
63
+
64
+ const changelogMetaSchema = z
65
+ .object({
66
+ category: z.string().optional(),
67
+ date: dateSchema.optional(),
68
+ version: z.string().optional(),
69
+ })
70
+ .strict();
71
+
72
+ /** Frontmatter accepted on any content page. */
73
+ const pageMetaBaseSchema = z
74
+ .object({
75
+ changelog: changelogMetaSchema.optional(),
76
+ /** Publish date for feed-backed content like blog/changelog. */
77
+ date: dateSchema.optional(),
78
+ deprecated: z.boolean().default(false),
79
+ description: z.string().optional(),
80
+ draft: z.boolean().default(false),
81
+ groups: z.union([z.string(), z.array(z.string())]).optional(),
82
+ hidden: z.boolean().default(false),
83
+ hideApiMarker: z.boolean().default(false),
84
+ hideFooterPagination: z.boolean().optional(),
85
+ icon: iconName.optional(),
86
+ iconType: z.string().optional(),
87
+ keywords: z.array(z.string()).optional(),
88
+ /** Overrides the git-derived last-modified date when `lastModified` is on. */
89
+ lastModified: dateSchema.optional(),
90
+ mode: z.string().optional(),
91
+ noindex: z.boolean().default(false),
92
+ public: z.boolean().optional(),
93
+ rss: z.boolean().optional(),
94
+ search: searchMetaSchema.default({}),
95
+ seo: seoMetaSchema.default({}),
96
+ sidebar: sidebarMetaSchema.default({}),
97
+ sidebarTitle: z.string().optional(),
98
+ slug: z.string().optional(),
99
+ tag: z.string().optional(),
100
+ title: z.string().optional(),
101
+ type: z.string().default("doc"),
102
+ })
103
+ .strict();
104
+
105
+ export const pageMetaSchema = pageMetaBaseSchema;
106
+
107
+ export type PageMeta = z.infer<typeof pageMetaBaseSchema>;
108
+ export type PageMetaInput = z.input<typeof pageMetaBaseSchema>;
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Folder meta (meta.ts)
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * How a sidebar group renders:
116
+ * - `flat`: a non-collapsible header with its items listed beneath (default).
117
+ * - `group`: a collapsible `<details>` disclosure.
118
+ * - `page`: a single row that drills into a sub-panel showing only this group's
119
+ * items, with a back arrow at the top.
120
+ */
121
+ const sidebarDisplaySchema = z.enum(["flat", "group", "page"]);
122
+ export type SidebarDisplay = z.infer<typeof sidebarDisplaySchema>;
123
+
124
+ export const folderMetaSchema = z
125
+ .object({
126
+ collapsed: z.boolean().optional(),
127
+ display: sidebarDisplaySchema.optional(),
128
+ icon: iconName.optional(),
129
+ order: z.number().optional(),
130
+ /** Explicit child ordering by slug segment (without numeric prefix). */
131
+ pages: z.array(z.string()).optional(),
132
+ title: z.string().optional(),
133
+ })
134
+ .strict();
135
+
136
+ export type FolderMeta = z.infer<typeof folderMetaSchema>;
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Project config (blume.config.ts)
140
+ // ---------------------------------------------------------------------------
141
+
142
+ const logoConfigSchema = z.union([
143
+ z.string(),
144
+ z
145
+ .object({
146
+ alt: z.string().optional(),
147
+ dark: z.string().optional(),
148
+ href: z.string().optional(),
149
+ light: z.string().optional(),
150
+ })
151
+ .strict(),
152
+ ]);
153
+
154
+ const faviconConfigSchema = z.union([
155
+ z.string(),
156
+ z
157
+ .object({
158
+ dark: z.string().optional(),
159
+ light: z.string().optional(),
160
+ })
161
+ .strict(),
162
+ ]);
163
+
164
+ const bannerColorSchema = z
165
+ .object({
166
+ dark: z.string().optional(),
167
+ light: z.string().optional(),
168
+ })
169
+ .strict()
170
+ .refine((value) => value.dark !== undefined || value.light !== undefined, {
171
+ message: "Banner color requires at least one of light or dark.",
172
+ });
173
+
174
+ /** Site-wide announcement banner: a string, or text with an optional link. */
175
+ const bannerConfigSchema = z.union([
176
+ z.string(),
177
+ z
178
+ .object({
179
+ /** Background color override (Mintlify compatibility). */
180
+ color: bannerColorSchema.optional(),
181
+ content: z.string(),
182
+ /** Show a dismiss button; the choice is remembered per visitor. */
183
+ dismissible: z.boolean().default(false),
184
+ /** Stable key for remembering dismissal; defaults to the content. */
185
+ id: z.string().optional(),
186
+ link: z
187
+ .object({ href: z.string(), text: z.string() })
188
+ .strict()
189
+ .optional(),
190
+ /** Tone (Mintlify compatibility). */
191
+ type: z.enum(["info", "warning", "critical"]).optional(),
192
+ })
193
+ .strict(),
194
+ ]);
195
+
196
+ /** A local filesystem content source. */
197
+ const filesystemSourceSchema = z
198
+ .object({
199
+ exclude: z.array(z.string()).default(["**/_*", "**/.*"]),
200
+ include: z.array(z.string()).default(["**/*.{md,mdx}"]),
201
+ /** Namespaces the source's routes under `/<prefix>/`. */
202
+ prefix: z.string().optional(),
203
+ root: z.string().default("docs"),
204
+ type: z.literal("filesystem"),
205
+ })
206
+ .strict();
207
+
208
+ /**
209
+ * Remote Markdown/MDX fetched over HTTP. Enumerate files either explicitly
210
+ * (`files` against a raw `url` base) or from a GitHub repo subtree (`github`).
211
+ * The token, when needed, comes from `GITHUB_TOKEN` — never inlined here.
212
+ */
213
+ const mdxRemoteSourceSchema = z
214
+ .object({
215
+ /** Explicit list of source-relative file paths to fetch from `url`. */
216
+ files: z.array(z.string()).optional(),
217
+ /** Enumerate a GitHub repo subtree via the git-trees API. */
218
+ github: z
219
+ .object({
220
+ owner: z.string(),
221
+ path: z.string().default(""),
222
+ ref: z.string().default("main"),
223
+ repo: z.string(),
224
+ })
225
+ .strict()
226
+ .optional(),
227
+ /** Glob patterns applied to enumerated refs. */
228
+ include: z.array(z.string()).default(["**/*.{md,mdx}"]),
229
+ /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
230
+ pollInterval: z.number().positive().optional(),
231
+ /** Namespaces the source's routes under `/<prefix>/`. */
232
+ prefix: z.string().optional(),
233
+ type: z.literal("mdx-remote"),
234
+ /** Raw base URL, e.g. `https://raw.githubusercontent.com/acme/sdk/main/docs`. */
235
+ url: z.string().optional(),
236
+ })
237
+ .strict();
238
+
239
+ /** A Sanity dataset queried with GROQ; Portable Text bodies become Markdown. */
240
+ const sanitySourceSchema = z.object({
241
+ /** Sanity API version (a date); default `2024-01-01`. */
242
+ apiVersion: z.string().optional(),
243
+ dataset: z.string(),
244
+ /** Field paths mapping a document onto Blume meta + body. */
245
+ fields: z
246
+ .object({
247
+ body: z.string().optional(),
248
+ description: z.string().optional(),
249
+ lastModified: z.string().optional(),
250
+ slug: z.string().optional(),
251
+ title: z.string().optional(),
252
+ })
253
+ .strict()
254
+ .optional(),
255
+ /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
256
+ pollInterval: z.number().positive().optional(),
257
+ prefix: z.string().optional(),
258
+ projectId: z.string(),
259
+ /** GROQ query selecting the documents to import. */
260
+ query: z.string(),
261
+ type: z.literal("sanity"),
262
+ });
263
+
264
+ /** A Notion database; pages become entries, blocks become MDX. */
265
+ const notionSourceSchema = z.object({
266
+ database: z.string(),
267
+ /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
268
+ pollInterval: z.number().positive().optional(),
269
+ prefix: z.string().optional(),
270
+ /** Notion property names mapped onto Blume meta. */
271
+ properties: z
272
+ .object({
273
+ description: z.string().optional(),
274
+ order: z.string().optional(),
275
+ slug: z.string().optional(),
276
+ status: z.string().optional(),
277
+ title: z.string().optional(),
278
+ })
279
+ .strict()
280
+ .optional(),
281
+ /** Status value treated as published; others map to `draft`. Default `Published`. */
282
+ publishedValue: z.string().optional(),
283
+ type: z.literal("notion"),
284
+ });
285
+
286
+ /**
287
+ * In-place Mintlify content (`docs.json` + MDX). Powers "bridge mode": Blume
288
+ * reads an unconverted Mintlify project, transforming each page to Blume MDX at
289
+ * scan time. Injected automatically by `loadConfig` when a `docs.json` is found
290
+ * and no `blume.config.*` exists; can also be configured explicitly.
291
+ */
292
+ const mintlifySourceSchema = z
293
+ .object({
294
+ /** Absolute path of the `docs.json`/`mint.json`, watched for changes in dev. */
295
+ configFile: z.string().optional(),
296
+ /** Patterns excluded from page discovery (Mintlify defaults are merged in). */
297
+ exclude: z.array(z.string()).default([]),
298
+ /** Glob patterns for Mintlify content files. */
299
+ include: z.array(z.string()).default(["**/*.{md,mdx}"]),
300
+ /** Namespaces the source's routes under `/<prefix>/`. */
301
+ prefix: z.string().optional(),
302
+ /** Content root, absolute or relative to the project root (Mintlify: `.`). */
303
+ root: z.string().default("."),
304
+ type: z.literal("mintlify"),
305
+ /** `docs.json` variables, inlined into content (`{{name}}`) at scan time. */
306
+ variables: z.record(z.string(), z.string()).default({}),
307
+ })
308
+ .strict();
309
+
310
+ /**
311
+ * A user-provided `ContentSource` instance, passed straight through from
312
+ * `blume.config.ts`. This is the extension point that lets adapters with custom
313
+ * serializers (or any backend) ship without their SDKs touching core.
314
+ */
315
+ const customSourceSchema = z.object({
316
+ source: z.custom<ContentSource>(
317
+ (val) =>
318
+ typeof val === "object" &&
319
+ val !== null &&
320
+ typeof (val as { load?: unknown }).load === "function" &&
321
+ typeof (val as { name?: unknown }).name === "string",
322
+ { message: "custom source must be a ContentSource (with name + load)" }
323
+ ),
324
+ type: z.literal("custom"),
325
+ });
326
+
327
+ /** A single configured content source. */
328
+ const contentSourceSchema = z.discriminatedUnion("type", [
329
+ filesystemSourceSchema,
330
+ mdxRemoteSourceSchema,
331
+ sanitySourceSchema,
332
+ notionSourceSchema,
333
+ mintlifySourceSchema,
334
+ customSourceSchema,
335
+ ]);
336
+
337
+ /** A resolved content-source config entry (post-defaults). */
338
+ export type ContentSourceConfig = z.infer<typeof contentSourceSchema>;
339
+
340
+ const contentConfigSchema = z
341
+ .object({
342
+ defaultType: z.string().default("doc"),
343
+ exclude: z.array(z.string()).default(["**/_*", "**/.*"]),
344
+ include: z.array(z.string()).default(["**/*.{md,mdx}"]),
345
+ pages: z.string().default("pages"),
346
+ root: z.string().default("docs"),
347
+ /**
348
+ * Pluggable content sources. When omitted, the top-level
349
+ * `root`/`include`/`exclude` desugar to one implicit filesystem source, so
350
+ * existing projects are unchanged.
351
+ */
352
+ sources: z.array(contentSourceSchema).optional(),
353
+ })
354
+ .strict();
355
+
356
+ const navTabSchema = z
357
+ .object({
358
+ icon: iconName.optional(),
359
+ items: z
360
+ .array(
361
+ z
362
+ .object({
363
+ description: z.string().optional(),
364
+ icon: iconName.optional(),
365
+ label: z.string(),
366
+ path: z.string(),
367
+ tag: z.string().optional(),
368
+ })
369
+ .strict()
370
+ )
371
+ .optional(),
372
+ label: z.string(),
373
+ path: z.string(),
374
+ })
375
+ .strict();
376
+
377
+ const navSelectorItemSchema = z
378
+ .object({
379
+ description: z.string().optional(),
380
+ icon: iconName.optional(),
381
+ label: z.string(),
382
+ path: z.string(),
383
+ tag: z.string().optional(),
384
+ })
385
+ .strict();
386
+
387
+ const navSelectorSchema = z
388
+ .object({
389
+ items: z.array(navSelectorItemSchema).default([]),
390
+ kind: z.enum(["dropdown", "language", "product", "version"]),
391
+ label: z.string(),
392
+ })
393
+ .strict();
394
+
395
+ const directoryModeSchema = z.enum(["accordion", "card", "none"]);
396
+ export type DirectoryMode = z.infer<typeof directoryModeSchema>;
397
+
398
+ /** A node in an explicit sidebar config: a page reference or a group/link. */
399
+ export type SidebarItemConfig =
400
+ | string
401
+ | {
402
+ label: string;
403
+ badge?: string;
404
+ directory?: DirectoryMode;
405
+ display?: SidebarDisplay;
406
+ href?: string;
407
+ icon?: string;
408
+ collapsed?: boolean;
409
+ items?: SidebarItemConfig[];
410
+ root?: string;
411
+ };
412
+
413
+ const sidebarItemSchema: z.ZodType<SidebarItemConfig> = z.lazy(() =>
414
+ z.union([
415
+ z.string(),
416
+ z
417
+ .object({
418
+ badge: z.string().optional(),
419
+ collapsed: z.boolean().optional(),
420
+ directory: directoryModeSchema.optional(),
421
+ display: sidebarDisplaySchema.optional(),
422
+ href: z.string().optional(),
423
+ icon: iconName.optional(),
424
+ items: z.array(sidebarItemSchema).optional(),
425
+ label: z.string(),
426
+ root: z.string().optional(),
427
+ })
428
+ .strict(),
429
+ ])
430
+ );
431
+
432
+ const sidebarVariantSchema = z
433
+ .object({
434
+ items: z.array(sidebarItemSchema).default([]),
435
+ path: z.string(),
436
+ })
437
+ .strict();
438
+
439
+ const navbarLinkTypeSchema = z.enum(["github", "discord"]);
440
+
441
+ const navbarLinkSchema = z
442
+ .object({
443
+ href: z.string(),
444
+ icon: iconName.optional(),
445
+ label: z.string().optional(),
446
+ type: navbarLinkTypeSchema.optional(),
447
+ })
448
+ .strict()
449
+ .refine((value) => value.label !== undefined || value.type !== undefined, {
450
+ message: "Navbar links require either label or type.",
451
+ });
452
+
453
+ const navbarPrimarySchema = z
454
+ .object({
455
+ href: z.string(),
456
+ label: z.string().optional(),
457
+ type: z.enum(["button", "github", "discord"]).default("button"),
458
+ })
459
+ .strict()
460
+ .refine((value) => value.label !== undefined || value.type !== "button", {
461
+ message: "Navbar primary button links require a label.",
462
+ });
463
+
464
+ const navbarConfigSchema = z
465
+ .object({
466
+ links: z.array(navbarLinkSchema).default([]),
467
+ primary: navbarPrimarySchema.optional(),
468
+ })
469
+ .strict();
470
+
471
+ const variablesConfigSchema = z
472
+ .record(z.string().regex(/^[A-Za-z0-9-]+$/u), z.string())
473
+ .default({});
474
+
475
+ /** A curated Google Font slug (see `theme/fonts.ts`). */
476
+ const fontSlug = z.string().refine(isFontSlug, (value) => ({
477
+ message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}.`,
478
+ }));
479
+
480
+ const themeConfigSchema = z
481
+ .object({
482
+ accent: z.string().default("blue"),
483
+ accentDark: z.string().optional(),
484
+ action: z.string().optional(),
485
+ background: z.string().optional(),
486
+ backgroundDark: z.string().optional(),
487
+ backgroundDecoration: z.enum(["gradient", "grid", "windows"]).optional(),
488
+ backgroundImage: z.string().optional(),
489
+ backgroundImageDark: z.string().optional(),
490
+ fonts: z
491
+ .object({
492
+ body: fontSlug.default("inter"),
493
+ display: fontSlug.default("inter-tight"),
494
+ mono: fontSlug.default("ibm-plex-mono"),
495
+ })
496
+ .strict()
497
+ .default({}),
498
+ layout: z.enum(["sidebar"]).default("sidebar"),
499
+ mode: z.enum(["system", "light", "dark"]).default("system"),
500
+ radius: z.enum(["none", "sm", "md", "lg"]).default("md"),
501
+ strict: z.boolean().default(false),
502
+ })
503
+ .strict();
504
+
505
+ const iconsConfigSchema = z
506
+ .object({
507
+ library: z.enum(["fontawesome", "lucide", "tabler"]).default("lucide"),
508
+ })
509
+ .strict();
510
+
511
+ /** Public credentials for the Algolia search backend (sync key is an env var). */
512
+ const algoliaSearchSchema = z
513
+ .object({
514
+ appId: z.string(),
515
+ indexName: z.string(),
516
+ searchApiKey: z.string(),
517
+ })
518
+ .strict();
519
+
520
+ /** Public credentials for the Orama Cloud search backend. */
521
+ const oramaCloudSearchSchema = z
522
+ .object({
523
+ apiKey: z.string(),
524
+ endpoint: z.string(),
525
+ /** Index id used by the build-time sync (with `ORAMA_PRIVATE_API_KEY`). */
526
+ indexId: z.string().optional(),
527
+ })
528
+ .strict();
529
+
530
+ /** Public credentials for a (self-hosted or cloud) Typesense backend. */
531
+ const typesenseSearchSchema = z
532
+ .object({
533
+ collection: z.string(),
534
+ host: z.string(),
535
+ port: z.number().int().positive().optional(),
536
+ protocol: z.enum(["http", "https"]).optional(),
537
+ searchApiKey: z.string(),
538
+ })
539
+ .strict();
540
+
541
+ /** Mixedbread semantic search: the store the server endpoint queries. */
542
+ const mixedbreadSearchSchema = z
543
+ .object({
544
+ storeId: z.string(),
545
+ })
546
+ .strict();
547
+
548
+ export const searchProviders = [
549
+ "orama",
550
+ "pagefind",
551
+ "flexsearch",
552
+ "algolia",
553
+ "orama-cloud",
554
+ "typesense",
555
+ "mixedbread",
556
+ "none",
557
+ ] as const;
558
+
559
+ /** Providers that need a config block, mapped to its `search.*` key. */
560
+ const PROVIDER_CONFIG_KEY = {
561
+ algolia: "algolia",
562
+ mixedbread: "mixedbread",
563
+ "orama-cloud": "oramaCloud",
564
+ typesense: "typesense",
565
+ } as const;
566
+
567
+ const searchConfigSchema = z
568
+ .object({
569
+ algolia: algoliaSearchSchema.optional(),
570
+ indexing: z
571
+ .object({
572
+ includeHiddenPages: z.boolean().default(false),
573
+ })
574
+ .strict()
575
+ .default({}),
576
+ mixedbread: mixedbreadSearchSchema.optional(),
577
+ oramaCloud: oramaCloudSearchSchema.optional(),
578
+ prompt: z.string().optional(),
579
+ provider: z.enum(searchProviders).default("orama"),
580
+ typesense: typesenseSearchSchema.optional(),
581
+ })
582
+ .strict()
583
+ .superRefine((value, ctx) => {
584
+ // Hosted providers can't work without their credentials; flag a missing
585
+ // block with a path so the diagnostic points at `search.<provider>`.
586
+ const field =
587
+ PROVIDER_CONFIG_KEY[value.provider as keyof typeof PROVIDER_CONFIG_KEY];
588
+ if (field && !value[field]) {
589
+ ctx.addIssue({
590
+ code: z.ZodIssueCode.custom,
591
+ message: `search.${field} is required when provider is "${value.provider}".`,
592
+ path: [field],
593
+ });
594
+ }
595
+ });
596
+
597
+ /** Ask AI backends. `gateway` (default) routes through the Vercel AI Gateway. */
598
+ export const askAiProviders = [
599
+ "gateway",
600
+ "openrouter",
601
+ "llmgateway",
602
+ "inkeep",
603
+ "openai-compatible",
604
+ ] as const;
605
+
606
+ const aiConfigSchema = z
607
+ .object({
608
+ ask: z
609
+ .object({
610
+ // Name of the env var holding the provider's API key; each provider has
611
+ // a sensible default, so this only needs setting to override it.
612
+ apiKeyEnv: z.string().optional(),
613
+ // Base URL of the backend. Required for `openai-compatible`; for the
614
+ // named providers it overrides the built-in preset.
615
+ baseUrl: z.string().url().optional(),
616
+ enabled: z.boolean().default(false),
617
+ model: z.string().default("openai/gpt-5.5"),
618
+ provider: z.enum(askAiProviders).default("gateway"),
619
+ })
620
+ .strict()
621
+ .superRefine((value, ctx) => {
622
+ // A generic OpenAI-compatible backend has no preset URL, so the user
623
+ // must supply one; the named providers fall back to their preset.
624
+ if (value.provider === "openai-compatible" && !value.baseUrl) {
625
+ ctx.addIssue({
626
+ code: z.ZodIssueCode.custom,
627
+ message:
628
+ 'ai.ask.baseUrl is required when provider is "openai-compatible".',
629
+ path: ["baseUrl"],
630
+ });
631
+ }
632
+ })
633
+ .optional(),
634
+ llmsTxt: z.boolean().default(false),
635
+ })
636
+ .strict();
637
+
638
+ const contextualOptionSchema = z.union([
639
+ z.string(),
640
+ z
641
+ .object({
642
+ description: z.string().optional(),
643
+ href: z.string().optional(),
644
+ icon: iconName.optional(),
645
+ title: z.string(),
646
+ })
647
+ .passthrough(),
648
+ ]);
649
+
650
+ const contextualConfigSchema = z
651
+ .object({
652
+ display: z.enum(["header", "toc"]).default("header"),
653
+ options: z.array(contextualOptionSchema).default([]),
654
+ })
655
+ .strict();
656
+
657
+ const footerConfigSchema = z
658
+ .object({
659
+ links: z
660
+ .array(
661
+ z
662
+ .object({
663
+ header: z.string().optional(),
664
+ items: z
665
+ .array(
666
+ z
667
+ .object({
668
+ href: z.string(),
669
+ label: z.string(),
670
+ })
671
+ .strict()
672
+ )
673
+ .default([]),
674
+ })
675
+ .strict()
676
+ )
677
+ .max(4)
678
+ .default([]),
679
+ socials: z.record(z.string(), z.string()).default({}),
680
+ })
681
+ .strict();
682
+
683
+ const chromeVariantSchema = z
684
+ .object({
685
+ banner: bannerConfigSchema.optional(),
686
+ footer: footerConfigSchema.optional(),
687
+ navbar: navbarConfigSchema.optional(),
688
+ path: z.string(),
689
+ })
690
+ .strict();
691
+
692
+ const navigationConfigSchema = z
693
+ .object({
694
+ chromeVariants: z.array(chromeVariantSchema).default([]),
695
+ /** Show a GitHub repo link in the header (requires `github` configured). */
696
+ repo: z.boolean().default(true),
697
+ selectors: z.array(navSelectorSchema).default([]),
698
+ /** Explicit sidebar override; when omitted the sidebar is generated. */
699
+ sidebar: z.array(sidebarItemSchema).optional(),
700
+ sidebarVariants: z.array(sidebarVariantSchema).default([]),
701
+ tabs: z.array(navTabSchema).optional(),
702
+ })
703
+ .strict();
704
+
705
+ export type AskAiProvider = (typeof askAiProviders)[number];
706
+ export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
707
+
708
+ // Reader-facing "Export" page action (PDF via print, EPUB via client-side
709
+ // generation). Off by default. Accepts a shorthand boolean to toggle both
710
+ // formats, or an object to enable them individually; both normalize to
711
+ // `{ epub, pdf }` so consumers read plain booleans.
712
+ const exportConfigSchema = z
713
+ .union([
714
+ z.boolean(),
715
+ z
716
+ .object({
717
+ epub: z.boolean().default(false),
718
+ pdf: z.boolean().default(false),
719
+ })
720
+ .strict(),
721
+ ])
722
+ .transform((value) =>
723
+ typeof value === "boolean" ? { epub: value, pdf: value } : value
724
+ );
725
+
726
+ const mcpConfigSchema = z
727
+ .object({
728
+ enabled: z.boolean().default(false),
729
+ /** Optional system hint passed to connecting agents. */
730
+ instructions: z.string().optional(),
731
+ /** Server name shown to clients; defaults to the site title. */
732
+ name: z.string().optional(),
733
+ route: z.string().default("/mcp"),
734
+ })
735
+ .strict();
736
+
737
+ /** A configured locale: ISO-ish code plus display metadata for the switcher. */
738
+ const localeSchema = z
739
+ .object({
740
+ code: z.string().min(1),
741
+ /** Text direction; drives `<html dir>` and a future RTL pass. */
742
+ dir: z.enum(["ltr", "rtl"]).default("ltr"),
743
+ label: z.string(),
744
+ })
745
+ .strict();
746
+
747
+ /**
748
+ * Internationalization. Opt-in: when absent, Blume is single-locale and behaves
749
+ * exactly as before. The default locale lives at the content root; other locales
750
+ * are top-level directories named by `code` (the `dir` parser).
751
+ */
752
+ const i18nConfigSchema = z
753
+ .object({
754
+ defaultLocale: z.string().default("en"),
755
+ /** Locale rendered for a missing translation; `null` disables fallback. */
756
+ fallbackLocale: z.string().nullable().optional(),
757
+ /** Drop the URL prefix for the default locale (`/`, `/fr/…`). Static-safe. */
758
+ hideDefaultLocalePrefix: z.boolean().default(true),
759
+ locales: z.array(localeSchema).min(1),
760
+ /** `"dir"`: locale directories (`fr/page.mdx`). `"dot"`: filename suffix (`page.fr.mdx`). */
761
+ parser: z.enum(["dir", "dot"]).default("dir"),
762
+ /** Per-locale UI string overrides: `{ fr: { search: { button: "…" } } }`. */
763
+ ui: uiLocaleOverridesSchema.optional(),
764
+ })
765
+ .strict()
766
+ .superRefine((value, ctx) => {
767
+ const codes = new Set(value.locales.map((locale) => locale.code));
768
+ if (!codes.has(value.defaultLocale)) {
769
+ ctx.addIssue({
770
+ code: z.ZodIssueCode.custom,
771
+ message: `i18n.defaultLocale "${value.defaultLocale}" must match one of i18n.locales.`,
772
+ path: ["defaultLocale"],
773
+ });
774
+ }
775
+ if (
776
+ value.fallbackLocale !== null &&
777
+ value.fallbackLocale !== undefined &&
778
+ !codes.has(value.fallbackLocale)
779
+ ) {
780
+ ctx.addIssue({
781
+ code: z.ZodIssueCode.custom,
782
+ message: `i18n.fallbackLocale "${value.fallbackLocale}" must match one of i18n.locales.`,
783
+ path: ["fallbackLocale"],
784
+ });
785
+ }
786
+ });
787
+
788
+ const analyticsScriptSchema = z
789
+ .object({
790
+ // Extra attributes (e.g. `data-domain`, `id`) spread onto the <script>.
791
+ attributes: z.record(z.string(), z.string()).optional(),
792
+ // Inline script body, mutually exclusive with `src`.
793
+ content: z.string().optional(),
794
+ // External script URL, mutually exclusive with `content`.
795
+ src: z.string().optional(),
796
+ // Load strategy for an external script.
797
+ strategy: z.enum(["async", "defer"]).optional(),
798
+ })
799
+ .strict()
800
+ .refine((value) => Boolean(value.src) !== Boolean(value.content), {
801
+ message: "An analytics script must set exactly one of `src` or `content`.",
802
+ });
803
+
804
+ const analyticsConfigSchema = z
805
+ .object({
806
+ posthog: z
807
+ .object({
808
+ host: z.string().optional(),
809
+ key: z.string(),
810
+ })
811
+ .strict()
812
+ .optional(),
813
+ // Escape hatch for any other provider (Plausible, Fathom, GA, Umami, …).
814
+ scripts: z.array(analyticsScriptSchema).optional(),
815
+ vercel: z.boolean().optional(),
816
+ })
817
+ .strict();
818
+
819
+ const deploymentConfigSchema = z
820
+ .object({
821
+ adapter: z
822
+ .enum(["vercel", "node", "netlify", "cloudflare"])
823
+ .nullable()
824
+ .default(null),
825
+ base: z.string().optional(),
826
+ output: z.enum(["static", "server"]).default("static"),
827
+ site: z.string().url().optional(),
828
+ })
829
+ .strict();
830
+
831
+ const redirectSchema = z
832
+ .object({
833
+ from: z.string(),
834
+ status: z
835
+ .union([z.literal(301), z.literal(302), z.literal(307), z.literal(308)])
836
+ .default(301),
837
+ to: z.string(),
838
+ })
839
+ .strict();
840
+
841
+ const ogConfigSchema = z
842
+ .object({
843
+ /**
844
+ * Generate a per-page Open Graph image. Defaults to on once a deployment
845
+ * site URL is known (set or auto-detected) and off otherwise, since
846
+ * `og:image` must be absolute to be useful to crawlers — resolved in
847
+ * `loadConfig`. An explicit value here always wins.
848
+ */
849
+ enabled: z.boolean().optional(),
850
+ })
851
+ .strict();
852
+
853
+ const rssConfigSchema = z
854
+ .object({
855
+ enabled: z.boolean().default(true),
856
+ /** Max items per feed, newest first. */
857
+ limit: z.number().int().positive().default(50),
858
+ /** Content types that each get a feed at `/<type>/rss.xml`. */
859
+ types: z.array(z.string()).default(["blog", "changelog"]),
860
+ })
861
+ .strict();
862
+
863
+ /** Discoverability features: OG images, feeds, sitemap, structured data. */
864
+ const seoConfigSchema = z
865
+ .object({
866
+ metatags: z.record(z.string(), z.string()).default({}),
867
+ og: ogConfigSchema.default({}),
868
+ /** Generate robots.txt (with a Sitemap reference when available). */
869
+ robots: z.boolean().default(true),
870
+ rss: rssConfigSchema.default({}),
871
+ /** Generate sitemap.xml (requires deployment.site). */
872
+ sitemap: z.boolean().default(true),
873
+ /** Emit schema.org JSON-LD in each page's <head>. */
874
+ structuredData: z.boolean().default(true),
875
+ })
876
+ .strict();
877
+
878
+ const githubConfigSchema = z
879
+ .object({
880
+ branch: z.string().default("main"),
881
+ /** Path from the repo root to the project root (for monorepos). */
882
+ dir: z.string().optional(),
883
+ owner: z.string(),
884
+ repo: z.string(),
885
+ })
886
+ .strict();
887
+
888
+ const codeBlockThemeSchema = z
889
+ .object({
890
+ dark: z.string().default("github-dark"),
891
+ light: z.string().default("github-light"),
892
+ })
893
+ .strict();
894
+
895
+ const codeBlocksConfigSchema = z
896
+ .object({
897
+ theme: codeBlockThemeSchema.default({}),
898
+ })
899
+ .strict();
900
+
901
+ /**
902
+ * "Last updated" timestamps for content pages. `false` (default) disables the
903
+ * feature; `true` derives each page's date from git history; an object selects
904
+ * the source explicitly. A page's `lastModified` frontmatter always wins.
905
+ */
906
+ const lastModifiedConfigSchema = z.union([
907
+ z.boolean(),
908
+ z.object({ type: z.enum(["git", "frontmatter"]).default("git") }).strict(),
909
+ ]);
910
+
911
+ /** Code-block rendering options (`markdown.code`). */
912
+ const codeConfigSchema = z
913
+ .object({
914
+ /**
915
+ * Show a brand language icon in the code-block header (TypeScript, Python,
916
+ * …). On by default; recognized languages only.
917
+ */
918
+ icons: z.boolean().default(true),
919
+ /**
920
+ * Syntax-highlight inline `` `code{:lang}` `` snippets. Off by default — most
921
+ * inline code (flags, file names) reads better plain; opt a snippet in with
922
+ * a trailing `{:lang}` marker.
923
+ */
924
+ inline: z.boolean().default(false),
925
+ /**
926
+ * Wrap long lines instead of scrolling horizontally. Off by default, so
927
+ * code keeps its original line breaks and overflows into a scroll area.
928
+ */
929
+ wrap: z.boolean().default(false),
930
+ })
931
+ .strict();
932
+
933
+ const markdownConfigSchema = z
934
+ .object({
935
+ /** Code-block rendering: language icons and line wrapping. */
936
+ code: codeConfigSchema.default({}),
937
+ codeBlocks: codeBlocksConfigSchema.default({}),
938
+ /**
939
+ * Wrap each `##`–`######` heading in a link to its own anchor so readers can
940
+ * click to copy, bookmark, or share a permalink to that section. On by
941
+ * default; set to `false` to render plain headings.
942
+ */
943
+ headingAnchors: z.boolean().default(true),
944
+ /**
945
+ * Make content images click-to-zoom (open in a lightbox). On by default;
946
+ * opt a single image out with `data-no-zoom`.
947
+ */
948
+ imageZoom: z.boolean().default(true),
949
+ /**
950
+ * Enable LaTeX math (`$…$` inline, `$$…$$` block) rendered with KaTeX.
951
+ * Off by default since `$` is common in prose, shell, and code. MDX only.
952
+ */
953
+ math: z.boolean().default(false),
954
+ })
955
+ .strict();
956
+
957
+ const stylingConfigSchema = z
958
+ .object({
959
+ eyebrows: z.enum(["breadcrumbs", "section"]).default("section"),
960
+ })
961
+ .strict();
962
+
963
+ /**
964
+ * A single spec rendered by the API reference (Scalar). `spec` is a local path
965
+ * or an `http(s)` URL; Scalar auto-detects OpenAPI vs AsyncAPI documents.
966
+ */
967
+ const openapiSourceSchema = z
968
+ .object({
969
+ /** Nav/section label for this source. */
970
+ label: z.string().optional(),
971
+ /** Per-source route; defaults to the block's `route` (or a derived path). */
972
+ route: z.string().optional(),
973
+ /** Local path or `http(s)` URL to the spec. */
974
+ spec: z.string(),
975
+ })
976
+ .strict();
977
+
978
+ export type OpenApiSource = z.infer<typeof openapiSourceSchema>;
979
+
980
+ /**
981
+ * OpenAPI reference, delegated wholesale to Scalar (`@scalar/astro`). The
982
+ * reference is a self-contained embed on its own route — it does not weave into
983
+ * Blume's sidebar, search, or llms. Set `enabled: true` to opt in.
984
+ */
985
+ const openapiConfigSchema = z
986
+ .object({
987
+ enabled: z.boolean().default(false),
988
+ /** Where the reference mounts. */
989
+ route: z.string().default("/reference"),
990
+ /** One or more specs; each renders on its own route by default. */
991
+ sources: z.array(openapiSourceSchema).default([]),
992
+ /** Shorthand for a single source: `sources: [{ spec }]`. */
993
+ spec: z.string().optional(),
994
+ /** Scalar theme name; defaults to a Blume-derived accent override. */
995
+ theme: z.string().optional(),
996
+ })
997
+ .strict();
998
+
999
+ /**
1000
+ * AsyncAPI reference. Same shape and Scalar pipeline as {@link openapiConfigSchema}
1001
+ * (Scalar auto-detects the document type); only the default `route` differs.
1002
+ */
1003
+ const asyncapiConfigSchema = z
1004
+ .object({
1005
+ enabled: z.boolean().default(false),
1006
+ route: z.string().default("/events"),
1007
+ sources: z.array(openapiSourceSchema).default([]),
1008
+ spec: z.string().optional(),
1009
+ theme: z.string().optional(),
1010
+ })
1011
+ .strict();
1012
+
1013
+ /** Full user-facing config schema. All fields optional with defaults. */
1014
+ export const blumeConfigSchema = z
1015
+ .object({
1016
+ ai: aiConfigSchema.default({}),
1017
+ analytics: analyticsConfigSchema.optional(),
1018
+ asyncapi: asyncapiConfigSchema.default({}),
1019
+ banner: bannerConfigSchema.optional(),
1020
+ content: contentConfigSchema.default({}),
1021
+ contextual: contextualConfigSchema.default({}),
1022
+ deployment: deploymentConfigSchema.default({}),
1023
+ description: z.string().optional(),
1024
+ export: exportConfigSchema.default(false),
1025
+ favicon: faviconConfigSchema.optional(),
1026
+ feedback: z.boolean().default(true),
1027
+ footer: footerConfigSchema.default({}),
1028
+ github: githubConfigSchema.optional(),
1029
+ i18n: i18nConfigSchema.optional(),
1030
+ icons: iconsConfigSchema.default({}),
1031
+ lastModified: lastModifiedConfigSchema.default(false),
1032
+ logo: logoConfigSchema.optional(),
1033
+ markdown: markdownConfigSchema.default({}),
1034
+ mcp: mcpConfigSchema.default({}),
1035
+ navbar: navbarConfigSchema.default({}),
1036
+ navigation: navigationConfigSchema.default({}),
1037
+ openapi: openapiConfigSchema.default({}),
1038
+ redirects: z.array(redirectSchema).default([]),
1039
+ search: searchConfigSchema.default({}),
1040
+ seo: seoConfigSchema.default({}),
1041
+ styling: stylingConfigSchema.default({}),
1042
+ theme: themeConfigSchema.default({}),
1043
+ title: z.string().default("Documentation"),
1044
+ variables: variablesConfigSchema,
1045
+ })
1046
+ .strict();
1047
+
1048
+ /** Resolved config: every field present after defaults are applied. */
1049
+ export type ResolvedConfig = z.infer<typeof blumeConfigSchema>;
1050
+ /** Resolved i18n block (present only when the project opts into i18n). */
1051
+ export type ResolvedI18nConfig = z.infer<typeof i18nConfigSchema>;
1052
+ /** A configured locale with display metadata. */
1053
+ export type LocaleConfig = z.infer<typeof localeSchema>;
1054
+ /** User-authored config: the shape accepted by `defineConfig`. */
1055
+ export type BlumeConfig = z.input<typeof blumeConfigSchema>;
1056
+ /** A configured search backend. */
1057
+ export type SearchProvider = (typeof searchProviders)[number];