blume 0.6.7 → 0.8.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 (211) hide show
  1. package/CHANGELOG.md +618 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/dist/cli/index.js +2609 -1041
  5. package/dist/cli/index.js.map +110 -103
  6. package/dist/types/ai/component-markdown.d.ts +34 -0
  7. package/dist/types/components/content/youtube.d.ts +18 -0
  8. package/dist/types/core/base-path.d.ts +47 -0
  9. package/dist/types/core/config-input.d.ts +110 -12
  10. package/dist/types/core/config.d.ts +6 -4
  11. package/dist/types/core/data.d.ts +4 -0
  12. package/dist/types/core/i18n-ui.d.ts +477 -135
  13. package/dist/types/core/schema.d.ts +309 -195
  14. package/dist/types/core/sources/types.d.ts +2 -0
  15. package/dist/types/core/types.d.ts +6 -1
  16. package/dist/types/index.d.ts +1 -0
  17. package/dist/types/openapi/references.d.ts +60 -0
  18. package/docs/01-quickstart.mdx +5 -2
  19. package/docs/02-deployment.mdx +24 -9
  20. package/docs/03-faq.mdx +46 -16
  21. package/docs/advanced/custom-pages.mdx +1 -1
  22. package/docs/advanced/skills.mdx +1 -1
  23. package/docs/configuration/ai.mdx +49 -10
  24. package/docs/configuration/customization.mdx +11 -0
  25. package/docs/configuration/index.mdx +33 -3
  26. package/docs/configuration/seo.mdx +2 -2
  27. package/docs/content/components.mdx +30 -3
  28. package/docs/content/i18n.mdx +1 -1
  29. package/docs/content/islands.mdx +8 -0
  30. package/docs/content/navigation.mdx +3 -3
  31. package/docs/content/sources.mdx +1 -1
  32. package/docs/content/syntax.mdx +17 -2
  33. package/docs/index.mdx +2 -2
  34. package/docs/reference/cli.mdx +8 -6
  35. package/package.json +15 -4
  36. package/skills/blume/SKILL.md +5 -3
  37. package/skills/blume-update-docs/SKILL.md +3 -2
  38. package/src/ai/agent-readability.ts +11 -5
  39. package/src/ai/ask-context.ts +7 -2
  40. package/src/ai/ask-data.ts +3 -0
  41. package/src/ai/ask.ts +12 -7
  42. package/src/ai/component-markdown.ts +461 -0
  43. package/src/ai/llms.ts +143 -23
  44. package/src/ai/markdown.ts +35 -6
  45. package/src/ai/mcp/data.ts +33 -8
  46. package/src/ai/mcp/discovery.ts +10 -3
  47. package/src/ai/mcp/server.ts +24 -7
  48. package/src/ai/visibility.ts +74 -0
  49. package/src/astro/component-slots.ts +16 -4
  50. package/src/astro/examples.ts +12 -7
  51. package/src/astro/generate.ts +393 -189
  52. package/src/astro/index.ts +5 -1
  53. package/src/astro/integration.ts +9 -5
  54. package/src/astro/islands.ts +11 -5
  55. package/src/astro/markdown-negotiation.ts +2 -2
  56. package/src/astro/pages.ts +89 -22
  57. package/src/astro/templates.ts +259 -25
  58. package/src/blume-modules.d.ts +8 -0
  59. package/src/cli/commands/build.ts +131 -38
  60. package/src/cli/commands/check.ts +1 -1
  61. package/src/cli/commands/dev.ts +71 -17
  62. package/src/cli/commands/doctor.ts +2 -2
  63. package/src/cli/commands/eject.ts +47 -19
  64. package/src/cli/commands/init.ts +120 -180
  65. package/src/cli/commands/preview.ts +4 -1
  66. package/src/cli/commands/validate.ts +44 -2
  67. package/src/cli/dev-lock.ts +34 -19
  68. package/src/cli/eject-scripts.ts +72 -0
  69. package/src/cli/env.ts +15 -5
  70. package/src/cli/init/questions.ts +158 -0
  71. package/src/cli/init/scaffold.ts +380 -0
  72. package/src/cli/required-secrets.ts +2 -1
  73. package/src/components/content/AccordionItem.astro +23 -4
  74. package/src/components/content/Badge.astro +3 -1
  75. package/src/components/content/Card.astro +4 -2
  76. package/src/components/content/CodeBlock.astro +3 -0
  77. package/src/components/content/Component.astro +30 -16
  78. package/src/components/content/Diff.astro +3 -1
  79. package/src/components/content/Step.astro +10 -1
  80. package/src/components/content/Tabs.astro +15 -3
  81. package/src/components/content/Tile.astro +2 -1
  82. package/src/components/content/Tooltip.astro +3 -1
  83. package/src/components/content/Update.astro +9 -2
  84. package/src/components/content/auto-type-table.ts +25 -9
  85. package/src/components/content/base-href.ts +33 -0
  86. package/src/components/content/changelog-element.ts +9 -2
  87. package/src/components/content/diff.ts +12 -6
  88. package/src/components/content/mermaid-element.ts +10 -2
  89. package/src/components/index.ts +23 -1
  90. package/src/components/islands/AskAI.astro +5 -2
  91. package/src/components/islands/ask-ai.tsx +68 -12
  92. package/src/components/islands/base-path.ts +28 -0
  93. package/src/components/islands/hooks.ts +44 -9
  94. package/src/components/layout/Banner.astro +12 -3
  95. package/src/components/layout/Breadcrumbs.astro +2 -1
  96. package/src/components/layout/Favicon.astro +3 -2
  97. package/src/components/layout/Header.astro +15 -5
  98. package/src/components/layout/LanguageSwitcher.astro +2 -1
  99. package/src/components/layout/Logo.astro +13 -4
  100. package/src/components/layout/NavSelector.astro +2 -1
  101. package/src/components/layout/NavTree.astro +22 -7
  102. package/src/components/layout/PageActions.astro +25 -10
  103. package/src/components/layout/PageFeedback.astro +4 -1
  104. package/src/components/layout/PageLayout.astro +51 -9
  105. package/src/components/layout/Pagination.astro +3 -2
  106. package/src/components/layout/ReferenceLayout.astro +8 -1
  107. package/src/components/layout/RootLayout.astro +74 -13
  108. package/src/components/layout/Search.astro +107 -27
  109. package/src/components/layout/nav-utils.ts +18 -10
  110. package/src/components/layout/search/algolia.ts +11 -2
  111. package/src/components/layout/search/endpoint.ts +11 -5
  112. package/src/components/layout/search/orama-cloud.ts +8 -2
  113. package/src/components/layout/search/pagefind.ts +3 -0
  114. package/src/components/layout/search/types.ts +5 -1
  115. package/src/components/layout/search/typesense.ts +4 -1
  116. package/src/components/layout/toc-element.ts +8 -2
  117. package/src/components/openapi/ApiTagOperations.astro +2 -1
  118. package/src/components/openapi/Operation.astro +47 -40
  119. package/src/components/openapi/RequestPanel.astro +8 -2
  120. package/src/components/openapi/helpers.ts +71 -3
  121. package/src/components/openapi/panel.ts +1 -1
  122. package/src/components/openapi/snippets.ts +25 -11
  123. package/src/core/base-path.ts +94 -0
  124. package/src/core/builtin-tags.ts +2 -0
  125. package/src/core/component-overrides.ts +103 -74
  126. package/src/core/config-input.ts +118 -17
  127. package/src/core/config.ts +8 -5
  128. package/src/core/content.ts +2 -0
  129. package/src/core/data.ts +4 -0
  130. package/src/core/diagnostics.ts +54 -34
  131. package/src/core/gitignore.ts +4 -1
  132. package/src/core/graph.ts +166 -88
  133. package/src/core/i18n-ui.ts +63 -3
  134. package/src/core/last-modified.ts +15 -6
  135. package/src/core/links.ts +69 -25
  136. package/src/core/manifest.ts +62 -45
  137. package/src/core/nav-diagnostics.ts +1 -1
  138. package/src/core/navigation.ts +144 -58
  139. package/src/core/package-json.ts +17 -2
  140. package/src/core/project-graph.ts +25 -15
  141. package/src/core/schema.ts +605 -620
  142. package/src/core/sources/assets.ts +6 -1
  143. package/src/core/sources/filesystem.ts +4 -0
  144. package/src/core/sources/github-releases.ts +2 -1
  145. package/src/core/sources/mdx-remote.ts +76 -63
  146. package/src/core/sources/normalize.ts +236 -91
  147. package/src/core/sources/notion.ts +27 -18
  148. package/src/core/sources/types.ts +2 -0
  149. package/src/core/tsconfig-aliases.ts +59 -30
  150. package/src/core/types.ts +6 -1
  151. package/src/core/ui-packs/ar.ts +1 -0
  152. package/src/core/ui-packs/bg.ts +1 -0
  153. package/src/core/ui-packs/bn.ts +1 -0
  154. package/src/core/ui-packs/ca.ts +1 -0
  155. package/src/core/ui-packs/cs.ts +1 -0
  156. package/src/core/ui-packs/da.ts +1 -0
  157. package/src/core/ui-packs/de.ts +1 -0
  158. package/src/core/ui-packs/el.ts +1 -0
  159. package/src/core/ui-packs/es.ts +1 -0
  160. package/src/core/ui-packs/fa.ts +1 -0
  161. package/src/core/ui-packs/fi.ts +1 -0
  162. package/src/core/ui-packs/fr.ts +2 -1
  163. package/src/core/ui-packs/he.ts +1 -0
  164. package/src/core/ui-packs/hi.ts +1 -0
  165. package/src/core/ui-packs/hr.ts +1 -0
  166. package/src/core/ui-packs/hu.ts +1 -0
  167. package/src/core/ui-packs/id.ts +1 -0
  168. package/src/core/ui-packs/it.ts +1 -0
  169. package/src/core/ui-packs/ja.ts +1 -0
  170. package/src/core/ui-packs/ko.ts +1 -0
  171. package/src/core/ui-packs/nl.ts +1 -0
  172. package/src/core/ui-packs/no.ts +1 -0
  173. package/src/core/ui-packs/pl.ts +1 -0
  174. package/src/core/ui-packs/pt-br.ts +1 -0
  175. package/src/core/ui-packs/pt.ts +1 -0
  176. package/src/core/ui-packs/ro.ts +1 -0
  177. package/src/core/ui-packs/ru.ts +1 -0
  178. package/src/core/ui-packs/sk.ts +1 -0
  179. package/src/core/ui-packs/sr.ts +1 -0
  180. package/src/core/ui-packs/sv.ts +1 -0
  181. package/src/core/ui-packs/th.ts +1 -0
  182. package/src/core/ui-packs/tr.ts +1 -0
  183. package/src/core/ui-packs/uk.ts +1 -0
  184. package/src/core/ui-packs/vi.ts +1 -0
  185. package/src/core/ui-packs/zh-tw.ts +1 -0
  186. package/src/core/ui-packs/zh.ts +1 -0
  187. package/src/deploy/adapter-output.ts +18 -8
  188. package/src/deploy/redirects.ts +25 -2
  189. package/src/deploy/robots.ts +6 -1
  190. package/src/deploy/rss.ts +10 -3
  191. package/src/deploy/sitemap.ts +59 -13
  192. package/src/index.ts +5 -0
  193. package/src/markdown/base-links.ts +60 -0
  194. package/src/markdown/code-title.ts +11 -14
  195. package/src/markdown/index.ts +46 -9
  196. package/src/markdown/inline-code.ts +14 -4
  197. package/src/markdown/package-commands.ts +10 -4
  198. package/src/markdown/themes.ts +24 -0
  199. package/src/openapi/model.ts +15 -5
  200. package/src/openapi/parse.ts +21 -0
  201. package/src/openapi/references.ts +75 -21
  202. package/src/openapi/render-mdx.ts +11 -6
  203. package/src/openapi/scalar.ts +32 -16
  204. package/src/openapi/source.ts +59 -10
  205. package/src/registry/eject.ts +247 -19
  206. package/src/registry/registry.ts +0 -3
  207. package/src/search/build.ts +3 -0
  208. package/src/search/documents.ts +36 -4
  209. package/src/search/sync/typesense.ts +6 -4
  210. package/src/seo/jsonld.ts +28 -17
  211. package/src/theme/entry.ts +85 -20
@@ -1,6 +1,9 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import type { ComponentMarkdown } from "../ai/component-markdown.ts";
4
+ import { normalizeRoute } from "../openapi/references.ts";
3
5
  import { FONT_SLUGS, isFontSlug } from "../theme/fonts.ts";
6
+ import { normalizeBasePath } from "./base-path.ts";
4
7
  import { uiLocaleOverridesSchema } from "./i18n-ui.ts";
5
8
  import type { ContentSource } from "./sources/types.ts";
6
9
 
@@ -18,6 +21,9 @@ import type { ContentSource } from "./sources/types.ts";
18
21
  /** Icon inputs in serializable contexts (frontmatter, meta files). */
19
22
  const iconName = z.string().min(1);
20
23
 
24
+ /** Default include glob for filesystem-backed content sources. */
25
+ const DEFAULT_CONTENT_GLOB = "**/*.{md,mdx}";
26
+
21
27
  const hydrationMode = z.enum(["load", "idle", "visible", "media", "only"]);
22
28
  export type HydrationMode = z.infer<typeof hydrationMode>;
23
29
 
@@ -33,41 +39,36 @@ const dateSchema = z
33
39
  // Page frontmatter
34
40
  // ---------------------------------------------------------------------------
35
41
 
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();
42
+ const sidebarMetaSchema = z.strictObject({
43
+ badge: z.string().optional(),
44
+ hidden: z.boolean().default(false),
45
+ icon: iconName.optional(),
46
+ label: z.string().optional(),
47
+ order: z.number().optional(),
48
+ });
55
49
 
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();
50
+ const seoMetaSchema = z.strictObject({
51
+ // blume bundles Zod 3; top-level `z.url()` is undefined at runtime and
52
+ // schemas must stay dual-compatible with consumer projects on Zod 4.
53
+ // oxlint-disable-next-line react-doctor/zod-v4-prefer-top-level-string-formats
54
+ canonical: z.string().url().optional(),
55
+ description: z.string().optional(),
56
+ image: z.string().optional(),
57
+ noindex: z.boolean().default(false),
58
+ title: z.string().optional(),
59
+ });
63
60
 
64
- const changelogMetaSchema = z
65
- .object({
66
- category: z.string().optional(),
67
- date: dateSchema.optional(),
68
- version: z.string().optional(),
69
- })
70
- .strict();
61
+ const searchMetaSchema = z.strictObject({
62
+ boost: z.number().optional(),
63
+ exclude: z.boolean().default(false),
64
+ tags: z.array(z.string()).optional(),
65
+ });
66
+
67
+ const changelogMetaSchema = z.strictObject({
68
+ category: z.string().optional(),
69
+ date: dateSchema.optional(),
70
+ version: z.string().optional(),
71
+ });
71
72
 
72
73
  /**
73
74
  * A post author: a bare name/handle, or an object with a name plus optional
@@ -85,34 +86,32 @@ const authorSchema = z.union([
85
86
  name: z.string(),
86
87
  url: z.string().optional(),
87
88
  })
88
- .passthrough(),
89
+ .catchall(z.unknown()),
89
90
  ]);
90
91
 
91
92
  /** Frontmatter accepted on any content page. */
92
- const pageMetaBaseSchema = z
93
- .object({
94
- /** Post author(s) for blog/changelog content; preserved, not yet rendered. */
95
- authors: z.union([authorSchema, z.array(authorSchema)]).optional(),
96
- changelog: changelogMetaSchema.optional(),
97
- /** Publish date for feed-backed content like blog/changelog. */
98
- date: dateSchema.optional(),
99
- deprecated: z.boolean().default(false),
100
- description: z.string().optional(),
101
- draft: z.boolean().default(false),
102
- hidden: z.boolean().default(false),
103
- icon: iconName.optional(),
104
- /** Overrides the git-derived last-modified date when `lastModified` is on. */
105
- lastModified: dateSchema.optional(),
106
- noindex: z.boolean().default(false),
107
- search: searchMetaSchema.default({}),
108
- seo: seoMetaSchema.default({}),
109
- sidebar: sidebarMetaSchema.default({}),
110
- slug: z.string().optional(),
111
- title: z.string().optional(),
112
- // No default: an absent `type` must fall through to `content.defaultType`.
113
- type: z.string().optional(),
114
- })
115
- .strict();
93
+ const pageMetaBaseSchema = z.strictObject({
94
+ /** Post author(s) for blog/changelog content; preserved, not yet rendered. */
95
+ authors: z.union([authorSchema, z.array(authorSchema)]).optional(),
96
+ changelog: changelogMetaSchema.optional(),
97
+ /** Publish date for feed-backed content like blog/changelog. */
98
+ date: dateSchema.optional(),
99
+ deprecated: z.boolean().default(false),
100
+ description: z.string().optional(),
101
+ draft: z.boolean().default(false),
102
+ hidden: z.boolean().default(false),
103
+ icon: iconName.optional(),
104
+ /** Overrides the git-derived last-modified date when `lastModified` is on. */
105
+ lastModified: dateSchema.optional(),
106
+ noindex: z.boolean().default(false),
107
+ search: searchMetaSchema.default({}),
108
+ seo: seoMetaSchema.default({}),
109
+ sidebar: sidebarMetaSchema.default({}),
110
+ slug: z.string().optional(),
111
+ title: z.string().optional(),
112
+ // No default: an absent `type` must fall through to `content.defaultType`.
113
+ type: z.string().optional(),
114
+ });
116
115
 
117
116
  export const pageMetaSchema = pageMetaBaseSchema;
118
117
 
@@ -133,16 +132,14 @@ export type PageMetaInput = z.input<typeof pageMetaBaseSchema>;
133
132
  const sidebarDisplaySchema = z.enum(["flat", "group", "page"]);
134
133
  export type SidebarDisplay = z.infer<typeof sidebarDisplaySchema>;
135
134
 
136
- export const folderMetaSchema = z
137
- .object({
138
- collapsed: z.boolean().optional(),
139
- icon: iconName.optional(),
140
- order: z.number().optional(),
141
- /** Explicit child ordering by slug segment (without numeric prefix). */
142
- pages: z.array(z.string()).optional(),
143
- title: z.string().optional(),
144
- })
145
- .strict();
135
+ export const folderMetaSchema = z.strictObject({
136
+ collapsed: z.boolean().optional(),
137
+ icon: iconName.optional(),
138
+ order: z.number().optional(),
139
+ /** Explicit child ordering by slug segment (without numeric prefix). */
140
+ pages: z.array(z.string()).optional(),
141
+ title: z.string().optional(),
142
+ });
146
143
 
147
144
  export type FolderMeta = z.infer<typeof folderMetaSchema>;
148
145
 
@@ -153,13 +150,11 @@ export type FolderMeta = z.infer<typeof folderMetaSchema>;
153
150
  /** The logo mark: a single image path/URL, or light/dark variants with alt text. */
154
151
  const logoImageSchema = z.union([
155
152
  z.string(),
156
- z
157
- .object({
158
- alt: z.string().optional(),
159
- dark: z.string().optional(),
160
- light: z.string().optional(),
161
- })
162
- .strict(),
153
+ z.strictObject({
154
+ alt: z.string().optional(),
155
+ dark: z.string().optional(),
156
+ light: z.string().optional(),
157
+ }),
163
158
  ]);
164
159
 
165
160
  /**
@@ -171,75 +166,63 @@ const logoImageSchema = z.union([
171
166
  */
172
167
  const logoConfigSchema = z.union([
173
168
  z.string(),
174
- z
175
- .object({
176
- href: z.string().optional(),
177
- image: logoImageSchema.optional(),
178
- text: z.string().optional(),
179
- })
180
- .strict(),
169
+ z.strictObject({
170
+ href: z.string().optional(),
171
+ image: logoImageSchema.optional(),
172
+ text: z.string().optional(),
173
+ }),
181
174
  ]);
182
175
 
183
176
  /** Site-wide announcement banner: a string, or text with an optional link. */
184
177
  const bannerConfigSchema = z.union([
185
178
  z.string(),
186
- z
187
- .object({
188
- content: z.string(),
189
- /** Show a dismiss button; the choice is remembered per visitor. */
190
- dismissible: z.boolean().default(false),
191
- /** Stable key for remembering dismissal; defaults to the content. */
192
- id: z.string().optional(),
193
- link: z
194
- .object({ href: z.string(), text: z.string() })
195
- .strict()
196
- .optional(),
197
- })
198
- .strict(),
179
+ z.strictObject({
180
+ content: z.string(),
181
+ /** Show a dismiss button; the choice is remembered per visitor. */
182
+ dismissible: z.boolean().default(false),
183
+ /** Stable key for remembering dismissal; defaults to the content. */
184
+ id: z.string().optional(),
185
+ link: z.strictObject({ href: z.string(), text: z.string() }).optional(),
186
+ }),
199
187
  ]);
200
188
 
201
189
  /** A local filesystem content source. */
202
- const filesystemSourceSchema = z
203
- .object({
204
- exclude: z.array(z.string()).default(["**/_*", "**/.*"]),
205
- include: z.array(z.string()).default(["**/*.{md,mdx}"]),
206
- /** Namespaces the source's routes under `/<prefix>/`. */
207
- prefix: z.string().optional(),
208
- root: z.string().default("docs"),
209
- type: z.literal("filesystem"),
210
- })
211
- .strict();
190
+ const filesystemSourceSchema = z.strictObject({
191
+ exclude: z.array(z.string()).default(["**/_*", "**/.*"]),
192
+ include: z.array(z.string()).default([DEFAULT_CONTENT_GLOB]),
193
+ /** Namespaces the source's routes under `/<prefix>/`. */
194
+ prefix: z.string().optional(),
195
+ root: z.string().default("docs"),
196
+ type: z.literal("filesystem"),
197
+ });
212
198
 
213
199
  /**
214
200
  * Remote Markdown/MDX fetched over HTTP. Enumerate files either explicitly
215
201
  * (`files` against a raw `url` base) or from a GitHub repo subtree (`github`).
216
202
  * The token, when needed, comes from `GITHUB_TOKEN` — never inlined here.
217
203
  */
218
- const mdxRemoteSourceSchema = z
219
- .object({
220
- /** Explicit list of source-relative file paths to fetch from `url`. */
221
- files: z.array(z.string()).optional(),
222
- /** Enumerate a GitHub repo subtree via the git-trees API. */
223
- github: z
224
- .object({
225
- owner: z.string(),
226
- path: z.string().default(""),
227
- ref: z.string().default("main"),
228
- repo: z.string(),
229
- })
230
- .strict()
231
- .optional(),
232
- /** Glob patterns applied to enumerated refs. */
233
- include: z.array(z.string()).default(["**/*.{md,mdx}"]),
234
- /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
235
- pollInterval: z.number().positive().optional(),
236
- /** Namespaces the source's routes under `/<prefix>/`. */
237
- prefix: z.string().optional(),
238
- type: z.literal("mdx-remote"),
239
- /** Raw base URL, e.g. `https://raw.githubusercontent.com/acme/sdk/main/docs`. */
240
- url: z.string().optional(),
241
- })
242
- .strict();
204
+ const mdxRemoteSourceSchema = z.strictObject({
205
+ /** Explicit list of source-relative file paths to fetch from `url`. */
206
+ files: z.array(z.string()).optional(),
207
+ /** Enumerate a GitHub repo subtree via the git-trees API. */
208
+ github: z
209
+ .strictObject({
210
+ owner: z.string(),
211
+ path: z.string().default(""),
212
+ ref: z.string().default("main"),
213
+ repo: z.string(),
214
+ })
215
+ .optional(),
216
+ /** Glob patterns applied to enumerated refs. */
217
+ include: z.array(z.string()).default([DEFAULT_CONTENT_GLOB]),
218
+ /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
219
+ pollInterval: z.number().positive().optional(),
220
+ /** Namespaces the source's routes under `/<prefix>/`. */
221
+ prefix: z.string().optional(),
222
+ type: z.literal("mdx-remote"),
223
+ /** Raw base URL, e.g. `https://raw.githubusercontent.com/acme/sdk/main/docs`. */
224
+ url: z.string().optional(),
225
+ });
243
226
 
244
227
  /** A Sanity dataset queried with GROQ; Portable Text bodies become Markdown. */
245
228
  const sanitySourceSchema = z.object({
@@ -248,14 +231,13 @@ const sanitySourceSchema = z.object({
248
231
  dataset: z.string(),
249
232
  /** Field paths mapping a document onto Blume meta + body. */
250
233
  fields: z
251
- .object({
234
+ .strictObject({
252
235
  body: z.string().optional(),
253
236
  description: z.string().optional(),
254
237
  lastModified: z.string().optional(),
255
238
  slug: z.string().optional(),
256
239
  title: z.string().optional(),
257
240
  })
258
- .strict()
259
241
  .optional(),
260
242
  /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
261
243
  pollInterval: z.number().positive().optional(),
@@ -274,14 +256,13 @@ const notionSourceSchema = z.object({
274
256
  prefix: z.string().optional(),
275
257
  /** Notion property names mapped onto Blume meta. */
276
258
  properties: z
277
- .object({
259
+ .strictObject({
278
260
  description: z.string().optional(),
279
261
  order: z.string().optional(),
280
262
  slug: z.string().optional(),
281
263
  status: z.string().optional(),
282
264
  title: z.string().optional(),
283
265
  })
284
- .strict()
285
266
  .optional(),
286
267
  /** Status value treated as published; others map to `draft`. Default `Published`. */
287
268
  publishedValue: z.string().optional(),
@@ -293,25 +274,23 @@ const notionSourceSchema = z.object({
293
274
  * notes become the changelog with no files to maintain. A private repo reads a
294
275
  * token from `GITHUB_TOKEN`; it is never inlined here.
295
276
  */
296
- const githubReleasesSourceSchema = z
297
- .object({
298
- /** Include draft releases (needs a token with repo write access). */
299
- drafts: z.boolean().optional(),
300
- /** Cap the number of releases materialized, newest-first. Default 100. */
301
- limit: z.number().positive().optional(),
302
- /** Repository owner (user or org). */
303
- owner: z.string(),
304
- /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
305
- pollInterval: z.number().positive().optional(),
306
- /** Namespaces the source's routes under `/<prefix>/`; e.g. `changelog`. */
307
- prefix: z.string().optional(),
308
- /** Include prereleases. */
309
- prereleases: z.boolean().optional(),
310
- /** Repository name. */
311
- repo: z.string(),
312
- type: z.literal("github-releases"),
313
- })
314
- .strict();
277
+ const githubReleasesSourceSchema = z.strictObject({
278
+ /** Include draft releases (needs a token with repo write access). */
279
+ drafts: z.boolean().optional(),
280
+ /** Cap the number of releases materialized, newest-first. Default 100. */
281
+ limit: z.number().positive().optional(),
282
+ /** Repository owner (user or org). */
283
+ owner: z.string(),
284
+ /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
285
+ pollInterval: z.number().positive().optional(),
286
+ /** Namespaces the source's routes under `/<prefix>/`; e.g. `changelog`. */
287
+ prefix: z.string().optional(),
288
+ /** Include prereleases. */
289
+ prereleases: z.boolean().optional(),
290
+ /** Repository name. */
291
+ repo: z.string(),
292
+ type: z.literal("github-releases"),
293
+ });
315
294
 
316
295
  /**
317
296
  * A user-provided `ContentSource` instance, passed straight through from
@@ -343,60 +322,50 @@ const contentSourceSchema = z.discriminatedUnion("type", [
343
322
  /** A resolved content-source config entry (post-defaults). */
344
323
  export type ContentSourceConfig = z.infer<typeof contentSourceSchema>;
345
324
 
346
- const contentConfigSchema = z
347
- .object({
348
- defaultType: z.string().default("doc"),
349
- exclude: z.array(z.string()).default(["**/_*", "**/.*"]),
350
- include: z.array(z.string()).default(["**/*.{md,mdx}"]),
351
- pages: z.string().default("pages"),
352
- root: z.string().default("docs"),
353
- /**
354
- * Pluggable content sources. When omitted, the top-level
355
- * `root`/`include`/`exclude` desugar to one implicit filesystem source, so
356
- * existing projects are unchanged.
357
- */
358
- sources: z.array(contentSourceSchema).optional(),
359
- })
360
- .strict();
361
-
362
- const navTabSchema = z
363
- .object({
364
- icon: iconName.optional(),
365
- items: z
366
- .array(
367
- z
368
- .object({
369
- description: z.string().optional(),
370
- icon: iconName.optional(),
371
- label: z.string(),
372
- path: z.string(),
373
- tag: z.string().optional(),
374
- })
375
- .strict()
376
- )
377
- .optional(),
378
- label: z.string(),
379
- path: z.string(),
380
- })
381
- .strict();
382
-
383
- const navSelectorItemSchema = z
384
- .object({
385
- description: z.string().optional(),
386
- icon: iconName.optional(),
387
- label: z.string(),
388
- path: z.string(),
389
- tag: z.string().optional(),
390
- })
391
- .strict();
325
+ const contentConfigSchema = z.strictObject({
326
+ defaultType: z.string().default("doc"),
327
+ exclude: z.array(z.string()).default(["**/_*", "**/.*"]),
328
+ include: z.array(z.string()).default([DEFAULT_CONTENT_GLOB]),
329
+ pages: z.string().default("pages"),
330
+ root: z.string().default("docs"),
331
+ /**
332
+ * Pluggable content sources. When omitted, the top-level
333
+ * `root`/`include`/`exclude` desugar to one implicit filesystem source, so
334
+ * existing projects are unchanged.
335
+ */
336
+ sources: z.array(contentSourceSchema).optional(),
337
+ });
392
338
 
393
- const navSelectorSchema = z
394
- .object({
395
- items: z.array(navSelectorItemSchema).default([]),
396
- kind: z.enum(["dropdown", "language", "product", "version"]),
397
- label: z.string(),
398
- })
399
- .strict();
339
+ const navTabSchema = z.strictObject({
340
+ icon: iconName.optional(),
341
+ items: z
342
+ .array(
343
+ z.strictObject({
344
+ description: z.string().optional(),
345
+ icon: iconName.optional(),
346
+ label: z.string(),
347
+ path: z.string(),
348
+ tag: z.string().optional(),
349
+ })
350
+ )
351
+ .optional(),
352
+ label: z.string(),
353
+ path: z.string(),
354
+ });
355
+
356
+ const navSelectorItemSchema = z.strictObject({
357
+ description: z.string().optional(),
358
+ icon: iconName.optional(),
359
+ label: z.string(),
360
+ path: z.string(),
361
+ tag: z.string().optional(),
362
+ });
363
+
364
+ const navSelectorSchema = z.strictObject({
365
+ items: z.array(navSelectorItemSchema).default([]),
366
+ kind: z.enum(["dropdown", "language", "product", "version"]),
367
+ label: z.string(),
368
+ });
400
369
 
401
370
  const directoryModeSchema = z.enum(["accordion", "card", "none"]);
402
371
  export type DirectoryMode = z.infer<typeof directoryModeSchema>;
@@ -419,26 +388,29 @@ export type SidebarItemConfig =
419
388
  const sidebarItemSchema: z.ZodType<SidebarItemConfig> = z.lazy(() =>
420
389
  z.union([
421
390
  z.string(),
422
- z
423
- .object({
424
- badge: z.string().optional(),
425
- collapsed: z.boolean().optional(),
426
- directory: directoryModeSchema.optional(),
427
- display: sidebarDisplaySchema.optional(),
428
- href: z.string().optional(),
429
- icon: iconName.optional(),
430
- items: z.array(sidebarItemSchema).optional(),
431
- label: z.string(),
432
- root: z.string().optional(),
433
- })
434
- .strict(),
391
+ z.strictObject({
392
+ badge: z.string().optional(),
393
+ collapsed: z.boolean().optional(),
394
+ directory: directoryModeSchema.optional(),
395
+ display: sidebarDisplaySchema.optional(),
396
+ href: z.string().optional(),
397
+ icon: iconName.optional(),
398
+ items: z.array(sidebarItemSchema).optional(),
399
+ label: z.string(),
400
+ root: z.string().optional(),
401
+ }),
435
402
  ])
436
403
  );
437
404
 
438
405
  /** A curated Google Font slug (see `theme/fonts.ts`). */
439
- const fontSlug = z.string().refine(isFontSlug, (value) => ({
440
- message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}.`,
441
- }));
406
+ const fontSlug = z.string().superRefine((value, ctx) => {
407
+ if (!isFontSlug(value)) {
408
+ ctx.addIssue({
409
+ code: z.ZodIssueCode.custom,
410
+ message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}.`,
411
+ });
412
+ }
413
+ });
442
414
 
443
415
  /**
444
416
  * An optional per-mode theme value: a string applies to both color modes; a
@@ -448,79 +420,69 @@ const fontSlug = z.string().refine(isFontSlug, (value) => ({
448
420
  const perModeValueSchema = z
449
421
  .union([
450
422
  z.string(),
451
- z
452
- .object({ dark: z.string().optional(), light: z.string().optional() })
453
- .strict(),
423
+ z.strictObject({
424
+ dark: z.string().optional(),
425
+ light: z.string().optional(),
426
+ }),
454
427
  ])
455
428
  .optional()
456
429
  .transform((value) =>
457
430
  typeof value === "string" ? { dark: value, light: value } : value
458
431
  );
459
432
 
460
- const themeConfigSchema = z
461
- .object({
462
- accent: z
463
- .union([
464
- z.string(),
465
- z.object({ dark: z.string(), light: z.string() }).strict(),
466
- ])
467
- .default("blue")
468
- .transform((value) =>
469
- typeof value === "string" ? { dark: value, light: value } : value
470
- ),
471
- action: z.string().optional(),
472
- background: perModeValueSchema,
473
- backgroundImage: perModeValueSchema,
474
- fonts: z
475
- .object({
476
- body: fontSlug.default("inter"),
477
- display: fontSlug.default("inter-tight"),
478
- mono: fontSlug.default("ibm-plex-mono"),
479
- })
480
- .strict()
481
- .default({}),
482
- layout: z.enum(["sidebar"]).default("sidebar"),
483
- mode: z.enum(["system", "light", "dark"]).default("system"),
484
- radius: z.enum(["none", "sm", "md", "lg"]).default("md"),
485
- })
486
- .strict();
433
+ const themeConfigSchema = z.strictObject({
434
+ accent: z
435
+ .union([
436
+ z.string(),
437
+ z.strictObject({ dark: z.string(), light: z.string() }),
438
+ ])
439
+ .default("blue")
440
+ .transform((value) =>
441
+ typeof value === "string" ? { dark: value, light: value } : value
442
+ ),
443
+ action: z.string().optional(),
444
+ background: perModeValueSchema,
445
+ backgroundImage: perModeValueSchema,
446
+ fonts: z
447
+ .strictObject({
448
+ body: fontSlug.default("inter"),
449
+ display: fontSlug.default("inter-tight"),
450
+ mono: fontSlug.default("ibm-plex-mono"),
451
+ })
452
+ .default({}),
453
+ layout: z.enum(["sidebar"]).default("sidebar"),
454
+ mode: z.enum(["system", "light", "dark"]).default("system"),
455
+ radius: z.enum(["none", "sm", "md", "lg"]).default("md"),
456
+ });
487
457
 
488
458
  /** Public credentials for the Algolia search backend (sync key is an env var). */
489
- const algoliaSearchSchema = z
490
- .object({
491
- appId: z.string(),
492
- indexName: z.string(),
493
- searchApiKey: z.string(),
494
- })
495
- .strict();
459
+ const algoliaSearchSchema = z.strictObject({
460
+ appId: z.string(),
461
+ indexName: z.string(),
462
+ searchApiKey: z.string(),
463
+ });
496
464
 
497
465
  /** Public credentials for the Orama Cloud search backend. */
498
- const oramaCloudSearchSchema = z
499
- .object({
500
- apiKey: z.string(),
501
- endpoint: z.string(),
502
- /** Index id used by the build-time sync (with `ORAMA_PRIVATE_API_KEY`). */
503
- indexId: z.string().optional(),
504
- })
505
- .strict();
466
+ const oramaCloudSearchSchema = z.strictObject({
467
+ apiKey: z.string(),
468
+ endpoint: z.string(),
469
+ /** Index id used by the build-time sync (with `ORAMA_PRIVATE_API_KEY`). */
470
+ indexId: z.string().optional(),
471
+ });
506
472
 
507
473
  /** Public credentials for a (self-hosted or cloud) Typesense backend. */
508
- const typesenseSearchSchema = z
509
- .object({
510
- collection: z.string(),
511
- host: z.string(),
512
- port: z.number().int().positive().optional(),
513
- protocol: z.enum(["http", "https"]).optional(),
514
- searchApiKey: z.string(),
515
- })
516
- .strict();
474
+ const typesenseSearchSchema = z.strictObject({
475
+ collection: z.string(),
476
+ host: z.string(),
477
+ port: z.number().int().positive().optional(),
478
+ protocol: z.enum(["http", "https"]).optional(),
479
+ searchApiKey: z.string(),
480
+ });
517
481
 
518
482
  /** Mixedbread semantic search: the store the server endpoint queries. */
519
- const mixedbreadSearchSchema = z
520
- .object({
521
- storeId: z.string(),
522
- })
523
- .strict();
483
+ const mixedbreadSearchSchema = z.strictObject({
484
+ storeId: z.string(),
485
+ });
524
486
 
525
487
  export const searchProviders = [
526
488
  "orama",
@@ -542,20 +504,18 @@ const PROVIDER_CONFIG_KEY = {
542
504
  } as const;
543
505
 
544
506
  const searchConfigSchema = z
545
- .object({
507
+ .strictObject({
546
508
  algolia: algoliaSearchSchema.optional(),
547
509
  indexing: z
548
- .object({
510
+ .strictObject({
549
511
  includeHiddenPages: z.boolean().default(false),
550
512
  })
551
- .strict()
552
513
  .default({}),
553
514
  mixedbread: mixedbreadSearchSchema.optional(),
554
515
  oramaCloud: oramaCloudSearchSchema.optional(),
555
516
  provider: z.enum(searchProviders).default("orama"),
556
517
  typesense: typesenseSearchSchema.optional(),
557
518
  })
558
- .strict()
559
519
  .superRefine((value, ctx) => {
560
520
  // Hosted providers can't work without their credentials; flag a missing
561
521
  // block with a path so the diagnostic points at `search.<provider>`.
@@ -579,95 +539,112 @@ export const askAiProviders = [
579
539
  "openai-compatible",
580
540
  ] as const;
581
541
 
582
- const aiConfigSchema = z
583
- .object({
584
- ask: z
585
- .object({
586
- // Name of the env var holding the provider's API key; each provider has
587
- // a sensible default, so this only needs setting to override it.
588
- apiKeyEnv: z.string().optional(),
589
- // Base URL of the backend. Required for `openai-compatible`; for the
590
- // named providers it overrides the built-in preset.
591
- baseUrl: z.string().url().optional(),
592
- enabled: z.boolean().default(false),
593
- model: z.string().default("openai/gpt-5.5"),
594
- provider: z.enum(askAiProviders).default("gateway"),
595
- // Empty-state prompts shown before the first question. Each renders as a
596
- // clickable suggestion; `icon` is an optional Lucide name beside it.
597
- suggestions: z
598
- .array(
599
- z
600
- .object({
601
- icon: iconName.optional(),
602
- label: z.string().min(1),
603
- })
604
- .strict()
605
- )
606
- .default([]),
607
- })
608
- .strict()
609
- .superRefine((value, ctx) => {
610
- // A generic OpenAI-compatible backend has no preset URL, so the user
611
- // must supply one; the named providers fall back to their preset.
612
- if (value.provider === "openai-compatible" && !value.baseUrl) {
613
- ctx.addIssue({
614
- code: z.ZodIssueCode.custom,
615
- message:
616
- 'ai.ask.baseUrl is required when provider is "openai-compatible".',
617
- path: ["baseUrl"],
618
- });
619
- }
542
+ const aiConfigSchema = z.strictObject({
543
+ ask: z
544
+ .strictObject({
545
+ // Name of the env var holding the provider's API key; each provider has
546
+ // a sensible default, so this only needs setting to override it.
547
+ apiKeyEnv: z.string().optional(),
548
+ // Base URL of the backend. Required for `openai-compatible`; for the
549
+ // named providers it overrides the built-in preset.
550
+ // blume bundles Zod 3; top-level `z.url()` is undefined at runtime.
551
+ // oxlint-disable-next-line react-doctor/zod-v4-prefer-top-level-string-formats
552
+ baseUrl: z.string().url().optional(),
553
+ enabled: z.boolean().default(false),
554
+ model: z.string().default("openai/gpt-5.5"),
555
+ provider: z.enum(askAiProviders).default("gateway"),
556
+ // Empty-state prompts shown before the first question. Each renders as a
557
+ // clickable suggestion; `icon` is an optional Lucide name beside it.
558
+ suggestions: z
559
+ .array(
560
+ z.strictObject({
561
+ icon: iconName.optional(),
562
+ label: z.string().min(1),
563
+ })
564
+ )
565
+ .default([]),
566
+ })
567
+ .superRefine((value, ctx) => {
568
+ // A generic OpenAI-compatible backend has no preset URL, so the user
569
+ // must supply one; the named providers fall back to their preset.
570
+ if (value.provider === "openai-compatible" && !value.baseUrl) {
571
+ ctx.addIssue({
572
+ code: z.ZodIssueCode.custom,
573
+ message:
574
+ 'ai.ask.baseUrl is required when provider is "openai-compatible".',
575
+ path: ["baseUrl"],
576
+ });
577
+ }
578
+ })
579
+ .optional(),
580
+ /**
581
+ * `llms.txt`/`llms-full.txt` emission. A bare boolean toggles it; the object
582
+ * form adds `openapi: false` to keep generated API reference pages out of
583
+ * both files (e.g. when the configured spec is example content).
584
+ */
585
+ llmsTxt: z
586
+ .union([
587
+ z.boolean(),
588
+ z.strictObject({
589
+ enabled: z.boolean().default(true),
590
+ openapi: z.boolean().default(true),
591
+ }),
592
+ ])
593
+ .default(true)
594
+ .transform((value) =>
595
+ typeof value === "boolean" ? { enabled: value, openapi: true } : value
596
+ ),
597
+ // Serializers for the agent-facing Markdown downlevel (the `.md` mirror,
598
+ // llms-full.txt, MCP get_page), keyed by JSX name. Functions live here —
599
+ // not in components.tsx — because the config file is executed at build
600
+ // time while the components file is only statically analyzed. A same-name
601
+ // entry replaces the built-in serializer.
602
+ markdownComponents: z
603
+ .record(
604
+ z.custom<ComponentMarkdown>((value) => typeof value === "function", {
605
+ message: "Expected a serializer function.",
620
606
  })
621
- .optional(),
622
- llmsTxt: z.boolean().default(true),
623
- })
624
- .strict();
607
+ )
608
+ .default({}),
609
+ });
625
610
 
626
611
  /**
627
612
  * A pinned link rendered above the sidebar sections — a blog, changelog, or
628
613
  * contact page that should always be reachable, regardless of the active tab.
629
614
  * `href` may be an external URL or an internal route.
630
615
  */
631
- const featuredLinkSchema = z
632
- .object({
633
- href: z.string(),
634
- icon: iconName.optional(),
635
- label: z.string(),
636
- })
637
- .strict();
638
-
639
- const navigationConfigSchema = z
640
- .object({
641
- /** Pinned links shown above the generated sidebar sections. */
642
- featured: z.array(featuredLinkSchema).default([]),
643
- /** Show a GitHub repo link in the header (requires `github` configured). */
644
- repo: z.boolean().default(true),
645
- selectors: z.array(navSelectorSchema).default([]),
646
- /**
647
- * Sidebar behavior. `display` sets how every group renders (a group in an
648
- * explicit `items` config may still override it); `items` is an explicit
649
- * sidebar — when omitted the sidebar is generated from the content tree.
650
- * A bare array is shorthand for `{ items }`.
651
- */
652
- sidebar: z
653
- .union([
654
- z.array(sidebarItemSchema),
655
- z
656
- .object({
657
- display: sidebarDisplaySchema.default("flat"),
658
- items: z.array(sidebarItemSchema).optional(),
659
- })
660
- .strict(),
661
- ])
662
- .default({})
663
- .transform((value) =>
664
- Array.isArray(value)
665
- ? { display: "flat" as const, items: value }
666
- : value
667
- ),
668
- tabs: z.array(navTabSchema).optional(),
669
- })
670
- .strict();
616
+ const featuredLinkSchema = z.strictObject({
617
+ href: z.string(),
618
+ icon: iconName.optional(),
619
+ label: z.string(),
620
+ });
621
+
622
+ const navigationConfigSchema = z.strictObject({
623
+ /** Pinned links shown above the generated sidebar sections. */
624
+ featured: z.array(featuredLinkSchema).default([]),
625
+ /** Show a GitHub repo link in the header (requires `github` configured). */
626
+ repo: z.boolean().default(true),
627
+ selectors: z.array(navSelectorSchema).default([]),
628
+ /**
629
+ * Sidebar behavior. `display` sets how every group renders (a group in an
630
+ * explicit `items` config may still override it); `items` is an explicit
631
+ * sidebar — when omitted the sidebar is generated from the content tree.
632
+ * A bare array is shorthand for `{ items }`.
633
+ */
634
+ sidebar: z
635
+ .union([
636
+ z.array(sidebarItemSchema),
637
+ z.strictObject({
638
+ display: sidebarDisplaySchema.default("flat"),
639
+ items: z.array(sidebarItemSchema).optional(),
640
+ }),
641
+ ])
642
+ .default({})
643
+ .transform((value) =>
644
+ Array.isArray(value) ? { display: "flat" as const, items: value } : value
645
+ ),
646
+ tabs: z.array(navTabSchema).optional(),
647
+ });
671
648
 
672
649
  export type AskAiProvider = (typeof askAiProviders)[number];
673
650
  export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
@@ -679,37 +656,35 @@ export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
679
656
  const exportConfigSchema = z
680
657
  .union([
681
658
  z.boolean(),
682
- z
683
- .object({
684
- epub: z.boolean().default(false),
685
- pdf: z.boolean().default(false),
686
- })
687
- .strict(),
659
+ z.strictObject({
660
+ epub: z.boolean().default(false),
661
+ pdf: z.boolean().default(false),
662
+ }),
688
663
  ])
689
664
  .transform((value) =>
690
665
  typeof value === "boolean" ? { epub: value, pdf: value } : value
691
666
  );
692
667
 
693
- const mcpConfigSchema = z
694
- .object({
695
- enabled: z.boolean().default(false),
696
- /** Optional system hint passed to connecting agents. */
697
- instructions: z.string().optional(),
698
- /** Server name shown to clients; defaults to the site title. */
699
- name: z.string().optional(),
700
- route: z.string().default("/mcp"),
701
- })
702
- .strict();
668
+ const mcpConfigSchema = z.strictObject({
669
+ enabled: z.boolean().default(false),
670
+ /** Optional system hint passed to connecting agents. */
671
+ instructions: z.string().optional(),
672
+ /** Server name shown to clients; defaults to the site title. */
673
+ name: z.string().optional(),
674
+ /**
675
+ * Normalized like `openapi.route`: a slash-less value would otherwise be
676
+ * string-concatenated onto the site origin (`https://acme.comdocs-mcp`).
677
+ */
678
+ route: z.string().default("/mcp").transform(normalizeRoute),
679
+ });
703
680
 
704
681
  /** A configured locale: ISO-ish code plus display metadata for the switcher. */
705
- const localeSchema = z
706
- .object({
707
- code: z.string().min(1),
708
- /** Text direction; drives `<html dir>` and a future RTL pass. */
709
- dir: z.enum(["ltr", "rtl"]).default("ltr"),
710
- label: z.string(),
711
- })
712
- .strict();
682
+ const localeSchema = z.strictObject({
683
+ code: z.string().min(1),
684
+ /** Text direction; drives `<html dir>` and a future RTL pass. */
685
+ dir: z.enum(["ltr", "rtl"]).default("ltr"),
686
+ label: z.string(),
687
+ });
713
688
 
714
689
  /**
715
690
  * Internationalization. Opt-in: when absent, Blume is single-locale and behaves
@@ -717,7 +692,7 @@ const localeSchema = z
717
692
  * are top-level directories named by `code` (the `dir` parser).
718
693
  */
719
694
  const i18nConfigSchema = z
720
- .object({
695
+ .strictObject({
721
696
  defaultLocale: z.string().default("en"),
722
697
  /** Locale rendered for a missing translation; `null` disables fallback. */
723
698
  fallbackLocale: z.string().nullable().optional(),
@@ -729,7 +704,6 @@ const i18nConfigSchema = z
729
704
  /** Per-locale UI string overrides: `{ fr: { search: { button: "…" } } }`. */
730
705
  ui: uiLocaleOverridesSchema.optional(),
731
706
  })
732
- .strict()
733
707
  .superRefine((value, ctx) => {
734
708
  const codes = new Set(value.locales.map((locale) => locale.code));
735
709
  if (!codes.has(value.defaultLocale)) {
@@ -753,7 +727,7 @@ const i18nConfigSchema = z
753
727
  });
754
728
 
755
729
  const analyticsScriptSchema = z
756
- .object({
730
+ .strictObject({
757
731
  // Extra attributes (e.g. `data-domain`, `id`) spread onto the <script>.
758
732
  attributes: z.record(z.string(), z.string()).optional(),
759
733
  // Inline script body, mutually exclusive with `src`.
@@ -763,69 +737,59 @@ const analyticsScriptSchema = z
763
737
  // Load strategy for an external script.
764
738
  strategy: z.enum(["async", "defer"]).optional(),
765
739
  })
766
- .strict()
767
740
  .refine((value) => Boolean(value.src) !== Boolean(value.content), {
768
741
  message: "An analytics script must set exactly one of `src` or `content`.",
769
742
  });
770
743
 
771
- const analyticsConfigSchema = z
772
- .object({
773
- posthog: z
774
- .object({
775
- host: z.string().optional(),
776
- key: z.string(),
777
- })
778
- .strict()
779
- .optional(),
780
- // Escape hatch for any other provider (Plausible, Fathom, GA, Umami, …).
781
- scripts: z.array(analyticsScriptSchema).optional(),
782
- vercel: z.boolean().optional(),
783
- })
784
- .strict();
785
-
786
- const deploymentConfigSchema = z
787
- .object({
788
- adapter: z
789
- .enum(["vercel", "node", "netlify", "cloudflare"])
790
- .nullable()
791
- .default(null),
792
- base: z.string().optional(),
793
- output: z.enum(["static", "server"]).default("static"),
794
- site: z.string().url().optional(),
795
- })
796
- .strict();
797
-
798
- const redirectSchema = z
799
- .object({
800
- from: z.string(),
801
- status: z
802
- .union([z.literal(301), z.literal(302), z.literal(307), z.literal(308)])
803
- .default(301),
804
- to: z.string(),
805
- })
806
- .strict();
807
-
808
- const ogConfigSchema = z
809
- .object({
810
- /**
811
- * Generate a per-page Open Graph image. Defaults to on once a deployment
812
- * site URL is known (set or auto-detected) and off otherwise, since
813
- * `og:image` must be absolute to be useful to crawlers — resolved in
814
- * `loadConfig`. An explicit value here always wins.
815
- */
816
- enabled: z.boolean().optional(),
817
- })
818
- .strict();
819
-
820
- const rssConfigSchema = z
821
- .object({
822
- enabled: z.boolean().default(true),
823
- /** Max items per feed, newest first. */
824
- limit: z.number().int().positive().default(50),
825
- /** Content types that each get a feed at `/<type>/rss.xml`. */
826
- types: z.array(z.string()).default(["blog", "changelog"]),
827
- })
828
- .strict();
744
+ const analyticsConfigSchema = z.strictObject({
745
+ posthog: z
746
+ .strictObject({
747
+ host: z.string().optional(),
748
+ key: z.string(),
749
+ })
750
+ .optional(),
751
+ // Escape hatch for any other provider (Plausible, Fathom, GA, Umami, …).
752
+ scripts: z.array(analyticsScriptSchema).optional(),
753
+ vercel: z.boolean().optional(),
754
+ });
755
+
756
+ const deploymentConfigSchema = z.strictObject({
757
+ adapter: z
758
+ .enum(["vercel", "node", "netlify", "cloudflare"])
759
+ .nullable()
760
+ .default(null),
761
+ base: z.string().optional(),
762
+ output: z.enum(["static", "server"]).default("static"),
763
+ // blume bundles Zod 3; top-level `z.url()` is undefined at runtime.
764
+ // oxlint-disable-next-line react-doctor/zod-v4-prefer-top-level-string-formats
765
+ site: z.string().url().optional(),
766
+ });
767
+
768
+ const redirectSchema = z.strictObject({
769
+ from: z.string(),
770
+ status: z
771
+ .union([z.literal(301), z.literal(302), z.literal(307), z.literal(308)])
772
+ .default(301),
773
+ to: z.string(),
774
+ });
775
+
776
+ const ogConfigSchema = z.strictObject({
777
+ /**
778
+ * Generate a per-page Open Graph image. Defaults to on once a deployment
779
+ * site URL is known (set or auto-detected) and off otherwise, since
780
+ * `og:image` must be absolute to be useful to crawlers — resolved in
781
+ * `loadConfig`. An explicit value here always wins.
782
+ */
783
+ enabled: z.boolean().optional(),
784
+ });
785
+
786
+ const rssConfigSchema = z.strictObject({
787
+ enabled: z.boolean().default(true),
788
+ /** Max items per feed, newest first. */
789
+ limit: z.number().int().positive().default(50),
790
+ /** Content types that each get a feed at `/<type>/rss.xml`. */
791
+ types: z.array(z.string()).default(["blog", "changelog"]),
792
+ });
829
793
 
830
794
  /**
831
795
  * robots.txt `Content-Signal` preferences — the emerging content-usage
@@ -835,13 +799,11 @@ const rssConfigSchema = z
835
799
  * - `aiInput` → `ai-input` (grounding / RAG use at answer time)
836
800
  * - `aiTrain` → `ai-train` (model training)
837
801
  */
838
- const contentSignalsObjectSchema = z
839
- .object({
840
- aiInput: z.boolean().default(true),
841
- aiTrain: z.boolean().default(true),
842
- search: z.boolean().default(true),
843
- })
844
- .strict();
802
+ const contentSignalsObjectSchema = z.strictObject({
803
+ aiInput: z.boolean().default(true),
804
+ aiTrain: z.boolean().default(true),
805
+ search: z.boolean().default(true),
806
+ });
845
807
 
846
808
  /**
847
809
  * Content signals accept a boolean shorthand or a per-signal object, and
@@ -863,49 +825,70 @@ const contentSignalsSchema = z
863
825
  });
864
826
 
865
827
  /** Discoverability features: OG images, feeds, sitemap, structured data. */
866
- const seoConfigSchema = z
867
- .object({
868
- /**
869
- * Emit `agent-readability.json` at the site root: a manifest that indexes
870
- * the agent-facing surface (llms.txt, Markdown mirrors, MCP server, feeds)
871
- * so agents can discover it without scraping HTML.
872
- */
873
- agentReadability: z.boolean().default(true),
874
- /** robots.txt `Content-Signal` usage declaration (on by default). */
875
- contentSignals: contentSignalsSchema.default(true),
876
- og: ogConfigSchema.default({}),
877
- /** Generate robots.txt (with a Sitemap reference when available). */
878
- robots: z.boolean().default(true),
879
- rss: rssConfigSchema.default({}),
880
- /** Generate sitemap.xml (requires deployment.site). */
881
- sitemap: z.boolean().default(true),
882
- /** Emit schema.org JSON-LD in each page's <head>. */
883
- structuredData: z.boolean().default(true),
884
- })
885
- .strict();
886
-
887
- const githubConfigSchema = z
888
- .object({
889
- branch: z.string().default("main"),
890
- /** Path from the repo root to the project root (for monorepos). */
891
- dir: z.string().optional(),
892
- owner: z.string(),
893
- repo: z.string(),
894
- })
895
- .strict();
828
+ const seoConfigSchema = z.strictObject({
829
+ /**
830
+ * Emit `agent-readability.json` at the site root: a manifest that indexes
831
+ * the agent-facing surface (llms.txt, Markdown mirrors, MCP server, feeds)
832
+ * so agents can discover it without scraping HTML.
833
+ */
834
+ agentReadability: z.boolean().default(true),
835
+ /** robots.txt `Content-Signal` usage declaration (on by default). */
836
+ contentSignals: contentSignalsSchema.default(true),
837
+ og: ogConfigSchema.default({}),
838
+ /** Generate robots.txt (with a Sitemap reference when available). */
839
+ robots: z.boolean().default(true),
840
+ rss: rssConfigSchema.default({}),
841
+ /** Generate sitemap.xml (requires deployment.site). */
842
+ sitemap: z.boolean().default(true),
843
+ /** Emit schema.org JSON-LD in each page's <head>. */
844
+ structuredData: z.boolean().default(true),
845
+ });
896
846
 
897
- const codeBlockThemeSchema = z
898
- .object({
899
- dark: z.string().default("github-dark"),
900
- light: z.string().default("github-light"),
901
- })
902
- .strict();
847
+ const githubConfigSchema = z.strictObject({
848
+ branch: z.string().default("main"),
849
+ /** Path from the repo root to the project root (for monorepos). */
850
+ dir: z.string().optional(),
851
+ owner: z.string(),
852
+ repo: z.string(),
853
+ });
903
854
 
904
- const codeBlocksConfigSchema = z
905
- .object({
906
- theme: codeBlockThemeSchema.default({}),
907
- })
908
- .strict();
855
+ const codeBlockThemeSchema = z.strictObject({
856
+ dark: z.string().default("github-dark"),
857
+ light: z.string().default("github-light"),
858
+ });
859
+
860
+ const codeBlocksConfigSchema = z.strictObject({
861
+ theme: codeBlockThemeSchema.default({}),
862
+ });
863
+
864
+ /**
865
+ * `<Component />` example previews. A string is shorthand for `{ source }`:
866
+ * where examples live, relative to the project root (default `examples`).
867
+ * `source` may be a glob (anything with `*`/`?`/`[]`/`{}`/`!`), in which case
868
+ * only matching files are discovered and each `<Component path>` key is
869
+ * relative to the glob's static prefix — use this for a registry layout that
870
+ * colocates component sources with their examples
871
+ * (`registry/<pkg>/**\/examples/*`), leaving the sources (which have no
872
+ * default export to wrap) out.
873
+ *
874
+ * `css` names a stylesheet (relative to the project root) injected into every
875
+ * preview frame after Blume's default tokens. Previews render in an isolated
876
+ * iframe that the site's docs styles never reach, so this is where design
877
+ * tokens for the previewed components live — e.g. shadcn variables and
878
+ * `@theme` mappings. Tailwind itself is already provided; the file should
879
+ * hold tokens and custom styles, not another `@import "tailwindcss"`.
880
+ */
881
+ const examplesConfigSchema = z
882
+ .union([
883
+ z.string(),
884
+ z.strictObject({
885
+ css: z.string().optional(),
886
+ source: z.string().default("examples"),
887
+ }),
888
+ ])
889
+ .transform((value): { css?: string; source: string } =>
890
+ typeof value === "string" ? { source: value } : value
891
+ );
909
892
 
910
893
  /**
911
894
  * "Last updated" timestamps for content pages. `false` (default) disables the
@@ -914,58 +897,63 @@ const codeBlocksConfigSchema = z
914
897
  */
915
898
  const lastModifiedConfigSchema = z.union([
916
899
  z.boolean(),
917
- z.object({ type: z.enum(["git", "frontmatter"]).default("git") }).strict(),
900
+ z.strictObject({ type: z.enum(["git", "frontmatter"]).default("git") }),
918
901
  ]);
919
902
 
920
903
  /** Code-block rendering options (`markdown.code`). */
921
- const codeConfigSchema = z
922
- .object({
923
- /**
924
- * Show a brand language icon in the code-block header (TypeScript, Python,
925
- * …). On by default; recognized languages only.
926
- */
927
- icons: z.boolean().default(true),
928
- /**
929
- * Wrap long lines instead of scrolling horizontally. Off by default, so
930
- * code keeps its original line breaks and overflows into a scroll area.
931
- */
932
- wrap: z.boolean().default(false),
933
- })
934
- .strict();
935
-
936
- const markdownConfigSchema = z
937
- .object({
938
- /** Code-block rendering: language icons and line wrapping. */
939
- code: codeConfigSchema.default({}),
940
- codeBlocks: codeBlocksConfigSchema.default({}),
941
- /**
942
- * Wrap each `##`–`######` heading in a link to its own anchor so readers can
943
- * click to copy, bookmark, or share a permalink to that section. On by
944
- * default; set to `false` to render plain headings.
945
- */
946
- headingAnchors: z.boolean().default(true),
947
- /**
948
- * Make content images click-to-zoom (open in a lightbox). On by default;
949
- * opt a single image out with `data-no-zoom`.
950
- */
951
- imageZoom: z.boolean().default(true),
952
- })
953
- .strict();
904
+ const codeConfigSchema = z.strictObject({
905
+ /**
906
+ * Show a brand language icon in the code-block header (TypeScript, Python,
907
+ * …). On by default; recognized languages only.
908
+ */
909
+ icons: z.boolean().default(true),
910
+ /**
911
+ * Wrap long lines instead of scrolling horizontally. Off by default, so
912
+ * code keeps its original line breaks and overflows into a scroll area.
913
+ */
914
+ wrap: z.boolean().default(false),
915
+ });
916
+
917
+ const markdownConfigSchema = z.strictObject({
918
+ /** Code-block rendering: language icons and line wrapping. */
919
+ code: codeConfigSchema.default({}),
920
+ codeBlocks: codeBlocksConfigSchema.default({}),
921
+ /**
922
+ * Wrap each `##`–`######` heading in a link to its own anchor so readers can
923
+ * click to copy, bookmark, or share a permalink to that section. On by
924
+ * default; set to `false` to render plain headings.
925
+ */
926
+ headingAnchors: z.boolean().default(true),
927
+ /**
928
+ * Make content images click-to-zoom (open in a lightbox). On by default;
929
+ * opt a single image out with `data-no-zoom`.
930
+ */
931
+ imageZoom: z.boolean().default(true),
932
+ });
933
+
934
+ /** React island behavior (`react`). */
935
+ const reactConfigSchema = z.strictObject({
936
+ /**
937
+ * Auto-memoize React components/hooks with the React Compiler
938
+ * (`babel-plugin-react-compiler`). On by default whenever React is enabled
939
+ * (a project `.tsx`/`.jsx`, a React island/example/override, or Ask AI); set
940
+ * to `false` to skip the compiler's babel pass.
941
+ */
942
+ compiler: z.boolean().default(true),
943
+ });
954
944
 
955
945
  /**
956
946
  * A single spec rendered by the API reference. `spec` is a local path or an
957
947
  * `http(s)` URL (OpenAPI for the Blume renderer; OpenAPI or AsyncAPI for Scalar).
958
948
  */
959
- const openapiSourceSchema = z
960
- .object({
961
- /** Nav/section label for this source. */
962
- label: z.string().optional(),
963
- /** Per-source route; defaults to the block's `route` (or a derived path). */
964
- route: z.string().optional(),
965
- /** Local path or `http(s)` URL to the spec. */
966
- spec: z.string(),
967
- })
968
- .strict();
949
+ const openapiSourceSchema = z.strictObject({
950
+ /** Nav/section label for this source. */
951
+ label: z.string().optional(),
952
+ /** Per-source route; defaults to the block's `route` (or a derived path). */
953
+ route: z.string().optional(),
954
+ /** Local path or `http(s)` URL to the spec. */
955
+ spec: z.string(),
956
+ });
969
957
 
970
958
  export type OpenApiSource = z.infer<typeof openapiSourceSchema>;
971
959
 
@@ -976,39 +964,35 @@ export type OpenApiSource = z.infer<typeof openapiSourceSchema>;
976
964
  * `renderer: "scalar"` to fall back to the embedded Scalar SPA (a single
977
965
  * self-contained route that doesn't weave into the sidebar or search).
978
966
  */
979
- const openapiConfigSchema = z
980
- .object({
981
- /** Code-sample languages shown per operation (Blume renderer). */
982
- codeSamples: z.array(z.string()).default(["curl", "js", "python"]),
983
- enabled: z.boolean().default(false),
984
- /** Start nested schema rows expanded rather than collapsed (Blume renderer). */
985
- expandSchemas: z.boolean().default(false),
986
- /** Who renders the reference: Blume's own UI, or the embedded Scalar SPA. */
987
- renderer: z.enum(["blume", "scalar"]).default("blume"),
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 (Scalar renderer only). */
995
- theme: z.string().optional(),
996
- })
997
- .strict();
967
+ const openapiConfigSchema = z.strictObject({
968
+ /** Code-sample languages shown per operation (Blume renderer). */
969
+ codeSamples: z.array(z.string()).default(["curl", "js", "python"]),
970
+ enabled: z.boolean().default(false),
971
+ /** Start nested schema rows expanded rather than collapsed (Blume renderer). */
972
+ expandSchemas: z.boolean().default(false),
973
+ /** Who renders the reference: Blume's own UI, or the embedded Scalar SPA. */
974
+ renderer: z.enum(["blume", "scalar"]).default("blume"),
975
+ /** Where the reference mounts. */
976
+ route: z.string().default("/reference"),
977
+ /** One or more specs; each renders on its own route by default. */
978
+ sources: z.array(openapiSourceSchema).default([]),
979
+ /** Shorthand for a single source: `sources: [{ spec }]`. */
980
+ spec: z.string().optional(),
981
+ /** Scalar theme name (Scalar renderer only). */
982
+ theme: z.string().optional(),
983
+ });
998
984
 
999
985
  /**
1000
986
  * AsyncAPI reference. Same shape and Scalar pipeline as {@link openapiConfigSchema}
1001
987
  * (Scalar auto-detects the document type); only the default `route` differs.
1002
988
  */
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();
989
+ const asyncapiConfigSchema = z.strictObject({
990
+ enabled: z.boolean().default(false),
991
+ route: z.string().default("/events"),
992
+ sources: z.array(openapiSourceSchema).default([]),
993
+ spec: z.string().optional(),
994
+ theme: z.string().optional(),
995
+ });
1012
996
 
1013
997
  /** Full user-facing config schema. All fields optional with defaults. */
1014
998
  /**
@@ -1019,12 +1003,10 @@ const asyncapiConfigSchema = z
1019
1003
  const tocConfigSchema = z
1020
1004
  .union([
1021
1005
  z.boolean(),
1022
- z
1023
- .object({
1024
- maxHeadingLevel: z.number().int().min(1).max(6).optional(),
1025
- minHeadingLevel: z.number().int().min(1).max(6).optional(),
1026
- })
1027
- .strict(),
1006
+ z.strictObject({
1007
+ maxHeadingLevel: z.number().int().min(1).max(6).optional(),
1008
+ minHeadingLevel: z.number().int().min(1).max(6).optional(),
1009
+ }),
1028
1010
  ])
1029
1011
  .default(true)
1030
1012
  .transform((value) => {
@@ -1038,47 +1020,50 @@ const tocConfigSchema = z
1038
1020
  };
1039
1021
  });
1040
1022
 
1041
- export const blumeConfigSchema = z
1042
- .object({
1043
- ai: aiConfigSchema.default({}),
1044
- analytics: analyticsConfigSchema.optional(),
1045
- asyncapi: asyncapiConfigSchema.default({}),
1046
- banner: bannerConfigSchema.optional(),
1047
- content: contentConfigSchema.default({}),
1048
- deployment: deploymentConfigSchema.default({}),
1049
- description: z.string().optional(),
1050
- /**
1051
- * Where `<Component path>` resolves live previews and their source from,
1052
- * relative to the project root. Defaults to the `examples` directory; point
1053
- * it elsewhere when examples live outside a top-level `examples/`.
1054
- *
1055
- * May be a glob (anything with `*`/`?`/`[]`/`{}`/`!`), in which case only
1056
- * matching files are discovered and a `<Component path>` key is relative to
1057
- * the glob's static prefix. Use this for a registry layout that colocates
1058
- * component sources with their examples — `registry/<pkg>/**\/examples/*`
1059
- * targets just the examples, leaving the sources (which have no default
1060
- * export to wrap) out, so the registry needn't be forked into its own
1061
- * examples directory.
1062
- */
1063
- examples: z.string().default("examples"),
1064
- export: exportConfigSchema.default(false),
1065
- feedback: z.boolean().default(true),
1066
- github: githubConfigSchema.optional(),
1067
- i18n: i18nConfigSchema.optional(),
1068
- lastModified: lastModifiedConfigSchema.default(false),
1069
- logo: logoConfigSchema.optional(),
1070
- markdown: markdownConfigSchema.default({}),
1071
- mcp: mcpConfigSchema.default({}),
1072
- navigation: navigationConfigSchema.default({}),
1073
- openapi: openapiConfigSchema.default({}),
1074
- redirects: z.array(redirectSchema).default([]),
1075
- search: searchConfigSchema.default({}),
1076
- seo: seoConfigSchema.default({}),
1077
- theme: themeConfigSchema.default({}),
1078
- title: z.string().default("Documentation"),
1079
- toc: tocConfigSchema,
1080
- })
1081
- .strict();
1023
+ export const blumeConfigSchema = z.strictObject({
1024
+ ai: aiConfigSchema.default({}),
1025
+ analytics: analyticsConfigSchema.optional(),
1026
+ asyncapi: asyncapiConfigSchema.default({}),
1027
+ banner: bannerConfigSchema.optional(),
1028
+ /**
1029
+ * Site-wide mount point prepended to every generated route (e.g. `/docs`),
1030
+ * while staying invisible to the sidebar/nav tree. Distinct from a per-source
1031
+ * `prefix` (which creates a group) and from `deployment.base` (Astro's
1032
+ * host-subdirectory base); the two compose. Normalized to `""` or `/seg`.
1033
+ */
1034
+ basePath: z
1035
+ .string()
1036
+ .optional()
1037
+ .transform((value) => normalizeBasePath(value)),
1038
+ content: contentConfigSchema.default({}),
1039
+ deployment: deploymentConfigSchema.default({}),
1040
+ description: z.string().optional(),
1041
+ /**
1042
+ * Where `<Component path>` resolves live previews and their source from.
1043
+ * A string is shorthand for `{ source }` — the directory (or glob, for
1044
+ * colocated registry layouts) under the project root that holds example
1045
+ * files. The object form adds `css`: a stylesheet injected into every
1046
+ * preview frame (design tokens, shadcn variables, `@theme` mappings).
1047
+ */
1048
+ examples: examplesConfigSchema.default("examples"),
1049
+ export: exportConfigSchema.default(false),
1050
+ feedback: z.boolean().default(true),
1051
+ github: githubConfigSchema.optional(),
1052
+ i18n: i18nConfigSchema.optional(),
1053
+ lastModified: lastModifiedConfigSchema.default(false),
1054
+ logo: logoConfigSchema.optional(),
1055
+ markdown: markdownConfigSchema.default({}),
1056
+ mcp: mcpConfigSchema.default({}),
1057
+ navigation: navigationConfigSchema.default({}),
1058
+ openapi: openapiConfigSchema.default({}),
1059
+ react: reactConfigSchema.default({}),
1060
+ redirects: z.array(redirectSchema).default([]),
1061
+ search: searchConfigSchema.default({}),
1062
+ seo: seoConfigSchema.default({}),
1063
+ theme: themeConfigSchema.default({}),
1064
+ title: z.string().default("Documentation"),
1065
+ toc: tocConfigSchema,
1066
+ });
1082
1067
 
1083
1068
  /** Resolved config: every field present after defaults are applied. */
1084
1069
  export type ResolvedConfig = z.infer<typeof blumeConfigSchema>;