blume 2.0.2 → 2.0.3

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 (293) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
  3. package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
  4. package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
  5. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
  6. package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
  7. package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
  8. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
  9. package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
  10. package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
  11. package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
  12. package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
  13. package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
  14. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  15. package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
  16. package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
  17. package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
  18. package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
  19. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  20. package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
  21. package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
  22. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
  23. package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
  24. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  25. package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
  26. package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
  27. package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
  28. package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
  29. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  30. package/dist/cli/chunk-e04dxsz1.js +39 -0
  31. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  32. package/dist/cli/{chunk-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
  33. package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
  34. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  35. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  36. package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
  37. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  38. package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
  39. package/dist/cli/chunk-j85vccga.js.map +36 -0
  40. package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
  41. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
  42. package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
  43. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
  44. package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
  45. package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
  46. package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
  47. package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
  48. package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
  49. package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
  50. package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
  51. package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
  52. package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
  53. package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
  54. package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
  55. package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
  56. package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
  57. package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
  58. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  59. package/dist/cli/index.js +20 -18
  60. package/dist/cli/index.js.map +3 -3
  61. package/dist/types/ai/link-headers.d.ts +8 -1
  62. package/dist/types/ai/openapi-components.d.ts +5 -2
  63. package/dist/types/ai/relative-links.d.ts +9 -7
  64. package/dist/types/ai/skills.d.ts +4 -1
  65. package/dist/types/ai/tar.d.ts +1 -3
  66. package/dist/types/analytics/index.d.ts +2 -0
  67. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  68. package/dist/types/analytics/schema.d.ts +14 -0
  69. package/dist/types/astro/integration.d.ts +3 -2
  70. package/dist/types/core/base-path.d.ts +21 -7
  71. package/dist/types/core/config-input.d.ts +9 -7
  72. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  73. package/dist/types/core/heading-markers.d.ts +5 -7
  74. package/dist/types/core/i18n.d.ts +2 -0
  75. package/dist/types/core/last-modified.d.ts +10 -0
  76. package/dist/types/core/meta.d.ts +8 -0
  77. package/dist/types/core/schema.d.ts +7 -0
  78. package/dist/types/core/sources/lower.d.ts +29 -16
  79. package/dist/types/core/sources/normalize.d.ts +13 -1
  80. package/dist/types/core/sources/watch.d.ts +12 -5
  81. package/dist/types/core/standard-schema.d.ts +5 -0
  82. package/dist/types/deploy/artifacts.d.ts +6 -4
  83. package/dist/types/deploy/headers.d.ts +5 -0
  84. package/dist/types/deploy/platforms/types.d.ts +8 -0
  85. package/dist/types/deploy/redirects.d.ts +24 -13
  86. package/dist/types/markdown/directives.d.ts +62 -0
  87. package/dist/types/markdown/features.d.ts +21 -0
  88. package/dist/types/markdown/mdast.d.ts +63 -0
  89. package/dist/types/openapi/asyncapi.d.ts +4 -2
  90. package/docs/02-deployment.mdx +4 -2
  91. package/docs/08-faq.mdx +1 -1
  92. package/docs/advanced/custom-pages.mdx +3 -3
  93. package/docs/cli/audit.mdx +17 -1
  94. package/docs/cli/evals.mdx +3 -3
  95. package/docs/cli/translate.mdx +3 -3
  96. package/docs/cli/version.mdx +1 -1
  97. package/docs/configuration/analytics.mdx +20 -1
  98. package/docs/configuration/customization.mdx +2 -0
  99. package/docs/configuration/search.mdx +1 -1
  100. package/docs/content/components.mdx +1 -1
  101. package/docs/content/frontmatter.mdx +1 -1
  102. package/docs/content/i18n.mdx +2 -0
  103. package/docs/content/index.mdx +4 -2
  104. package/docs/content/islands.mdx +1 -1
  105. package/docs/content/meta.mdx +3 -1
  106. package/docs/content/sources.mdx +1 -5
  107. package/docs/content/syntax.mdx +14 -0
  108. package/docs/content/versioning.mdx +1 -1
  109. package/docs/discoverability/agent-discovery.mdx +1 -1
  110. package/docs/discoverability/index.mdx +2 -1
  111. package/docs/discoverability/markdown.mdx +2 -2
  112. package/docs/discoverability/open-graph.mdx +5 -3
  113. package/docs/discoverability/rss.mdx +1 -1
  114. package/docs/references/asyncapi.mdx +1 -1
  115. package/docs/references/graphql.mdx +1 -1
  116. package/docs/references/openapi.mdx +5 -3
  117. package/package.json +1 -1
  118. package/skills/blume-migrate/references/fumadocs.md +1 -1
  119. package/skills/blume-migrate/references/nextra.md +1 -1
  120. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
  121. package/src/ai/agent-readability.ts +2 -2
  122. package/src/ai/ai-catalog.ts +6 -2
  123. package/src/ai/api/handlers.ts +2 -2
  124. package/src/ai/api/spec.ts +2 -2
  125. package/src/ai/api-catalog.ts +6 -2
  126. package/src/ai/changelog-markdown.ts +2 -2
  127. package/src/ai/link-headers.ts +14 -5
  128. package/src/ai/llms.ts +18 -7
  129. package/src/ai/mcp/discovery.ts +2 -2
  130. package/src/ai/mcp/query.ts +2 -2
  131. package/src/ai/mcp/server.ts +2 -2
  132. package/src/ai/openapi-components.ts +22 -6
  133. package/src/ai/relative-links.ts +42 -26
  134. package/src/ai/serializers.ts +2 -1
  135. package/src/ai/skills.ts +14 -1
  136. package/src/ai/tar.ts +139 -12
  137. package/src/analytics/head.ts +4 -0
  138. package/src/analytics/index.ts +5 -0
  139. package/src/analytics/one-dollar-stats.ts +86 -0
  140. package/src/analytics/schema.ts +2 -0
  141. package/src/astro/generate.ts +1 -1
  142. package/src/astro/include-hmr.ts +31 -12
  143. package/src/astro/include-refresh.ts +19 -8
  144. package/src/astro/integration.ts +108 -14
  145. package/src/astro/runtime-modules.ts +28 -13
  146. package/src/astro/templates.ts +81 -37
  147. package/src/audit/checks/links.ts +8 -1
  148. package/src/audit/graph.ts +3 -1
  149. package/src/audit/run.ts +12 -9
  150. package/src/audit/snapshot.ts +5 -0
  151. package/src/cli/args.ts +32 -0
  152. package/src/cli/commands/audit.ts +4 -1
  153. package/src/cli/commands/doctor.ts +3 -1
  154. package/src/cli/commands/eval.ts +19 -11
  155. package/src/cli/commands/translate.ts +9 -12
  156. package/src/cli/commands/validate.ts +3 -1
  157. package/src/cli/dev-lock.ts +157 -37
  158. package/src/cli/report-format.ts +11 -6
  159. package/src/components/colors.ts +19 -0
  160. package/src/components/content/Badge.astro +5 -3
  161. package/src/components/content/Component.astro +2 -2
  162. package/src/components/content/Tab.astro +0 -1
  163. package/src/components/content/Tabs.astro +4 -0
  164. package/src/components/content/badge-color.ts +5 -2
  165. package/src/components/content/base-href.ts +13 -36
  166. package/src/components/islands/assistant.tsx +21 -4
  167. package/src/components/islands/base-path.ts +47 -10
  168. package/src/components/islands/hooks.ts +35 -19
  169. package/src/components/islands/webmcp.ts +4 -2
  170. package/src/components/layout/Breadcrumbs.astro +5 -2
  171. package/src/components/layout/DiscoveryLinks.astro +9 -5
  172. package/src/components/layout/Header.astro +2 -2
  173. package/src/components/layout/LanguageSwitcher.astro +2 -2
  174. package/src/components/layout/NavSelector.astro +2 -2
  175. package/src/components/layout/NavTabMenu.astro +3 -3
  176. package/src/components/layout/NavTree.astro +16 -6
  177. package/src/components/layout/PageActions.astro +4 -3
  178. package/src/components/layout/PageLayout.astro +7 -6
  179. package/src/components/layout/Pagination.astro +3 -3
  180. package/src/components/layout/RootLayout.astro +5 -5
  181. package/src/components/layout/Search.astro +33 -13
  182. package/src/components/layout/VersionBanner.astro +2 -2
  183. package/src/components/layout/analytics-client.ts +12 -0
  184. package/src/components/layout/toc-active.ts +41 -0
  185. package/src/components/layout/toc-element.ts +8 -14
  186. package/src/components/openapi/ApiTagOperations.astro +2 -2
  187. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  188. package/src/components/openapi/GraphqlChip.astro +2 -2
  189. package/src/components/openapi/Operation.astro +2 -0
  190. package/src/components/openapi/Playground.astro +4 -4
  191. package/src/components/openapi/RequestPanel.astro +5 -2
  192. package/src/components/openapi/SchemaTable.astro +7 -0
  193. package/src/components/openapi/async.ts +38 -6
  194. package/src/components/openapi/helpers.ts +19 -9
  195. package/src/components/openapi/message-composer.ts +6 -1
  196. package/src/components/openapi/message.ts +17 -2
  197. package/src/components/openapi/operation-model.ts +81 -11
  198. package/src/components/openapi/panel.ts +4 -2
  199. package/src/components/openapi/param-style.ts +181 -0
  200. package/src/components/openapi/playground-client.ts +73 -16
  201. package/src/components/openapi/request.ts +190 -26
  202. package/src/components/openapi/schema-tree.ts +30 -24
  203. package/src/components/openapi/snippets.ts +85 -6
  204. package/src/components/openapi/ws-client.ts +18 -2
  205. package/src/core/base-path.ts +62 -16
  206. package/src/core/config-input.ts +9 -7
  207. package/src/core/diagnostics.ts +2 -0
  208. package/src/core/directive-diagnostics.ts +99 -0
  209. package/src/core/frontmatter.ts +21 -18
  210. package/src/core/heading-markers.ts +5 -18
  211. package/src/core/i18n.ts +31 -3
  212. package/src/core/last-modified.ts +25 -3
  213. package/src/core/locale-links.ts +5 -1
  214. package/src/core/meta.ts +33 -19
  215. package/src/core/navigation.ts +28 -8
  216. package/src/core/project-graph.ts +27 -5
  217. package/src/core/schema.ts +24 -4
  218. package/src/core/sources/contentful-rich-text.ts +27 -20
  219. package/src/core/sources/filesystem.ts +20 -2
  220. package/src/core/sources/github-releases.ts +25 -6
  221. package/src/core/sources/lexical.ts +23 -18
  222. package/src/core/sources/lower.ts +201 -34
  223. package/src/core/sources/mdx-remote.ts +51 -16
  224. package/src/core/sources/normalize.ts +325 -90
  225. package/src/core/sources/notion.ts +52 -25
  226. package/src/core/sources/obsidian.ts +23 -6
  227. package/src/core/sources/portable-text.ts +38 -21
  228. package/src/core/sources/strapi-blocks.ts +20 -16
  229. package/src/core/sources/watch.ts +20 -7
  230. package/src/core/standard-schema.ts +10 -6
  231. package/src/core/version-cut.ts +52 -15
  232. package/src/deploy/artifacts.ts +27 -6
  233. package/src/deploy/cloudflare-negotiation.ts +4 -12
  234. package/src/deploy/headers.ts +8 -4
  235. package/src/deploy/node-headers.ts +1 -1
  236. package/src/deploy/platforms/cloudflare.ts +12 -3
  237. package/src/deploy/platforms/netlify.ts +1 -0
  238. package/src/deploy/platforms/node.ts +1 -0
  239. package/src/deploy/platforms/static.ts +1 -0
  240. package/src/deploy/platforms/types.ts +8 -0
  241. package/src/deploy/platforms/vercel.ts +1 -0
  242. package/src/deploy/redirects.ts +54 -18
  243. package/src/deploy/robots.ts +2 -2
  244. package/src/deploy/rss.ts +4 -3
  245. package/src/deploy/sitemap.ts +7 -5
  246. package/src/markdown/base-links.ts +34 -36
  247. package/src/markdown/directives.ts +242 -36
  248. package/src/markdown/features.ts +17 -0
  249. package/src/markdown/index.ts +7 -6
  250. package/src/markdown/mdast.ts +5 -2
  251. package/src/markdown/relative-links.ts +12 -4
  252. package/src/og/card.ts +129 -5
  253. package/src/og/derive.ts +41 -31
  254. package/src/og/index.ts +1 -0
  255. package/src/openapi/asyncapi.ts +4 -2
  256. package/src/openapi/model.ts +22 -14
  257. package/src/openapi/proxy.ts +63 -10
  258. package/src/openapi/render-mdx.ts +41 -2
  259. package/src/registry/eject.ts +181 -24
  260. package/src/search/adapters/version-scope.ts +30 -0
  261. package/src/search/documents.ts +67 -26
  262. package/src/search/popular.ts +2 -1
  263. package/src/seo/jsonld.ts +7 -3
  264. package/src/translate/meta.ts +68 -24
  265. package/src/translate/run.ts +3 -3
  266. package/src/translate/validate.ts +4 -1
  267. package/src/translate/work-list.ts +56 -17
  268. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  269. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  270. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  271. package/dist/cli/chunk-esh98wmb.js +0 -23
  272. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  273. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  274. package/dist/cli/chunk-j85scx15.js.map +0 -10
  275. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  276. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  277. /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
  278. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
  279. /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
  280. /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
  281. /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
  282. /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
  283. /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
  284. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
  285. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
  286. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
  287. /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
  288. /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
  289. /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
  290. /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
  291. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
  292. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
  293. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
@@ -69,8 +69,10 @@ const DOCS_PATHS = new Map(
69
69
  BLUME_TRANSLATE_MISSING: "/docs/cli/translate",
70
70
  BLUME_TRANSLATE_STALE: "/docs/cli/translate",
71
71
  BLUME_UNKNOWN_COMPONENT: "/docs/configuration/customization",
72
+ BLUME_UNKNOWN_DIRECTIVE: "/docs/content/syntax",
72
73
  BLUME_UNKNOWN_ICON: DOCS_CONTENT_NAVIGATION,
73
74
  BLUME_UNKNOWN_OPTION: "/docs/cli",
75
+ BLUME_UNLOADABLE_FILE_NAME: "/docs/content",
74
76
  BLUME_WIKILINK_AMBIGUOUS: DOCS_CONTENT_SOURCES,
75
77
  BLUME_WIKILINK_UNRESOLVED: DOCS_CONTENT_SOURCES,
76
78
  BLUME_YARN_PNP: "/docs/quickstart",
@@ -0,0 +1,99 @@
1
+ import type { Nodes } from "mdast";
2
+ import { mdxToMdast } from "satteri";
3
+
4
+ import {
5
+ CALLOUT_ALIASES,
6
+ CALLOUT_TYPES,
7
+ calloutTypeFor,
8
+ } from "../markdown/directives.ts";
9
+ import { MDX_BODY_FEATURES } from "../markdown/features.ts";
10
+ import { strippedLineOffset } from "./sources/normalize.ts";
11
+ import type { SourceEntry } from "./sources/types.ts";
12
+ import type { Diagnostic } from "./types.ts";
13
+
14
+ // A line that may open a container directive: a colon fence and a name, after
15
+ // any quote or list indentation. A page is parsed only when one of these
16
+ // names something other than a callout, so pages without one cost a scan.
17
+ const CONTAINER_OPENING = /^[\t >]*:{3,}(?<name>[a-z][\w-]*)/gimu;
18
+
19
+ const codeList = (names: Iterable<string>): string =>
20
+ [...names].map((name) => `\`${name}\``).join(", ");
21
+
22
+ const CALLOUT_NAMES = `${codeList(CALLOUT_TYPES)} (or the aliases ${codeList(Object.keys(CALLOUT_ALIASES))})`;
23
+
24
+ /** A container directive that isn't a callout, and its line in `text`. */
25
+ interface UnknownContainer {
26
+ line: number;
27
+ name: string;
28
+ }
29
+
30
+ /**
31
+ * Every container directive in an `.mdx` body whose name isn't a callout, in
32
+ * source order. The body is parsed as the renderer reads it, so a fence in a
33
+ * code block or a `::: name` that isn't a directive at all never counts. A
34
+ * body MDX can't parse fails to render anyway, with its own error.
35
+ */
36
+ const unknownContainers = (text: string): UnknownContainer[] => {
37
+ const named = [...text.matchAll(CONTAINER_OPENING)].some(
38
+ (match) => calloutTypeFor(match.groups?.name ?? "") === null
39
+ );
40
+ if (!named) {
41
+ return [];
42
+ }
43
+ let tree: Nodes;
44
+ try {
45
+ tree = mdxToMdast(text, { features: MDX_BODY_FEATURES });
46
+ } catch {
47
+ return [];
48
+ }
49
+ const found: UnknownContainer[] = [];
50
+ const walk = (node: Nodes): void => {
51
+ if (
52
+ node.type === "containerDirective" &&
53
+ calloutTypeFor(node.name) === null
54
+ ) {
55
+ found.push({ line: node.position?.start.line ?? 1, name: node.name });
56
+ }
57
+ if ("children" in node) {
58
+ for (const child of node.children) {
59
+ walk(child);
60
+ }
61
+ }
62
+ };
63
+ walk(tree);
64
+ return found;
65
+ };
66
+
67
+ /**
68
+ * Warn about each `:::name` container in an `.mdx` entry that isn't a callout.
69
+ * The page keeps its content — the body renders between the literal `:::`
70
+ * lines (see `markdown/directives.ts`) — but a typo like `:::warnig`, or a
71
+ * `:::details` carried over from another docs tool, should read as the
72
+ * mistake it is rather than as a finished page. Lines point into the file
73
+ * the author wrote, a partial's own file for a container an `<include>`
74
+ * brought in.
75
+ */
76
+ export const unknownDirectiveDiagnostics = (
77
+ entry: SourceEntry,
78
+ sourceName: string
79
+ ): Diagnostic[] => {
80
+ if (entry.body.format !== "mdx") {
81
+ return [];
82
+ }
83
+ const page = entry.sourcePath ?? `${sourceName}:${entry.ref}`;
84
+ const offset =
85
+ entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
86
+ return unknownContainers(entry.expanded?.text ?? entry.body.text).map(
87
+ ({ line, name }) => {
88
+ const origin = entry.expanded?.origins[line - 1];
89
+ return {
90
+ code: "BLUME_UNKNOWN_DIRECTIVE",
91
+ file: origin?.file ?? page,
92
+ line: origin?.line ?? line + offset,
93
+ message: `\`:::${name}\` isn't a callout type, so the page shows its \`:::\` lines as written around its content.`,
94
+ severity: "warning",
95
+ suggestion: `Use a callout type — ${CALLOUT_NAMES} — or remove the \`:::\` lines to keep the content as plain prose.`,
96
+ };
97
+ }
98
+ );
99
+ };
@@ -19,13 +19,18 @@ type StringifyArgs = Parameters<typeof baseMatter.stringify>;
19
19
  /** Front matter data as gray-matter types it (`GrayMatterFile["data"]`). */
20
20
  type FrontMatterData = ReturnType<typeof baseMatter>["data"];
21
21
 
22
+ /** A YAML load result: a mapping (or list) Astro keeps as front matter data. */
23
+ const isYamlCollection = (
24
+ value: ReturnType<typeof load>
25
+ ): value is FrontMatterData => typeof value === "object" && value !== null;
26
+
22
27
  const yamlEngine = {
23
- // SAFETY: gray-matter's engine contract expects an object; a front matter
24
- // block is a YAML mapping, and js-yaml returns a scalar only for degenerate
25
- // input, which gray-matter treats the same way its bundled engine's output
26
- // is treated.
27
- parse: (input: string): object =>
28
- (load(input, { schema: YAML_SCHEMA }) ?? {}) as object,
28
+ // A block that loads to a scalar (`title` alone, or prose between two
29
+ // `---` lines) is no metadata: Astro reads it as `{}`, and so does Blume.
30
+ parse: (input: string): FrontMatterData => {
31
+ const value = load(input, { schema: YAML_SCHEMA });
32
+ return isYamlCollection(value) ? value : {};
33
+ },
29
34
  stringify: (data: FrontMatterData): string => dump(data),
30
35
  };
31
36
 
@@ -45,23 +50,21 @@ const withYamlEngine = <O extends { engines?: object } | undefined>(
45
50
 
46
51
  /**
47
52
  * True when a document's leading `---` line is a CommonMark thematic break,
48
- * not a front matter fence. Two shapes qualify (mirroring
49
- * `linesWithoutFrontMatter` in `sources/normalize.ts`):
50
- * - the next line is blank (or absent) — YAML metadata starts on the very
51
- * next line, so a gap means the body *opens* with a divider (e.g. a
52
- * Notion page whose first block is one);
53
- * - no closing `---` line follows — gray-matter would swallow the whole
54
- * document as one unclosed YAML block and hand it to js-yaml, which
55
- * crashes on ordinary Markdown (`> quote` → "a line break is expected").
53
+ * not a front matter fence: no closing `---` line follows. Astro's own front
54
+ * matter match requires that close too, while gray-matter would swallow the
55
+ * whole document as one unclosed YAML block and hand it to js-yaml, which
56
+ * crashes on ordinary Markdown (`> quote` → "a line break is expected").
57
+ *
58
+ * A blank line after the opening `---` is still front matter: Astro strips
59
+ * the block from the page either way, so reading it as a divider here would
60
+ * ignore its `draft`, `slug`, and `hidden` while its YAML leaked into search
61
+ * and llms.txt.
56
62
  */
57
63
  const opensWithThematicBreak = (input: string): boolean => {
58
- const [first = "", second] = input.split(/\r?\n/u, 2);
64
+ const [first = ""] = input.split(/\r?\n/u, 1);
59
65
  if (!/^-{3}\s*$/u.test(first)) {
60
66
  return false;
61
67
  }
62
- if (second === undefined || second.trim() === "") {
63
- return true;
64
- }
65
68
  // gray-matter closes the block at the next line-leading `---`; matching its
66
69
  // search exactly keeps this guard from firing on any document it parses.
67
70
  return !input.includes("\n---", 1);
@@ -55,17 +55,12 @@ export interface HeadingMarkers {
55
55
  * the heading count (mid-text brackets are ordinary prose); when a marker kind
56
56
  * repeats, the last one written wins.
57
57
  *
58
- * `isRefDefined` reports whether a link-reference definition exists for a
59
- * bracket's label (`toc`, `!toc`, `#id`): a defined bracket is a CommonMark
60
- * shortcut link, not a marker — the renderer sees a resolved `<a>` element
61
- * there and strips nothing at or before it, so the parse stops the same way.
62
- * The render-time hast plugin never passes it (resolved links are already
63
- * elements by the time it runs); the raw-source scanner does.
58
+ * Callers pass the heading's trailing text node after a Markdown parse, so a
59
+ * bracket that a link-reference definition turns into a CommonMark shortcut
60
+ * link (`[toc]` with a `[toc]: …` definition) is already an element there and
61
+ * never reaches this parse.
64
62
  */
65
- export const parseHeadingMarkers = (
66
- text: string,
67
- isRefDefined?: (label: string) => boolean
68
- ): HeadingMarkers => {
63
+ export const parseHeadingMarkers = (text: string): HeadingMarkers => {
69
64
  let remaining = text;
70
65
  let id: string | undefined;
71
66
  let toc: HeadingMarkers["toc"];
@@ -75,14 +70,6 @@ export const parseHeadingMarkers = (
75
70
  match = MARKER.exec(remaining)
76
71
  ) {
77
72
  const explicitId = match.groups.curlyId ?? match.groups.id;
78
- const label =
79
- explicitId === undefined
80
- ? `${match.groups.hide ?? ""}toc`
81
- : `#${explicitId}`;
82
- // A `{#id}` is never a shortcut link, whatever the definitions say.
83
- if (match.groups.curlyId === undefined && isRefDefined?.(label)) {
84
- break;
85
- }
86
73
  remaining = remaining.slice(0, match.index);
87
74
  if (explicitId === undefined) {
88
75
  // Stripping runs right-to-left, so keeping the first capture of each
package/src/core/i18n.ts CHANGED
@@ -219,11 +219,31 @@ export const localeTargetPath = (
219
219
  return `${folder}/${rel}`;
220
220
  };
221
221
 
222
+ /**
223
+ * The warning for a top-level folder named after the default locale
224
+ * (`docs/en/` when `defaultLocale` is `en`). Only non-default codes are
225
+ * locale folders — the default locale lives at the content root — so the
226
+ * folder is ordinary default-locale content, published one level down at
227
+ * `/en/…` (`/en/en/…` with the default prefix shown) and filled into every
228
+ * other locale at `/fr/en/…`.
229
+ */
230
+ const defaultLocaleFolderDiagnostic = (
231
+ folder: string,
232
+ i18n: ResolvedI18nConfig
233
+ ): Diagnostic => ({
234
+ code: "BLUME_I18N_DEFAULT_LOCALE_FOLDER",
235
+ message: `Folder "${folder}/" is named after the default locale, but "${i18n.defaultLocale}" pages live at the content root — its pages publish at ${localizeRoute(`/${folder}`, i18n.defaultLocale, i18n)}/… instead.`,
236
+ severity: "warning",
237
+ suggestion: `Move the contents of "${folder}/" up to the content root; only the other locales get a folder.`,
238
+ });
239
+
222
240
  /**
223
241
  * Warn about top-level content folders that look like a locale (a code Blume
224
242
  * recognizes) but aren't declared in `i18n.locales`. Without this they're
225
243
  * silently treated as default-locale content under a `/<code>/…` route, which
226
244
  * is almost never intended — usually a translation that wasn't registered.
245
+ * A folder named after the default locale gets the same treatment (see
246
+ * {@link defaultLocaleFolderDiagnostic}).
227
247
  */
228
248
  export const i18nDiagnostics = (
229
249
  pages: PageRecord[],
@@ -242,9 +262,17 @@ export const i18nDiagnostics = (
242
262
  // Inside a version snapshot the locale dir sits one level deeper
243
263
  // (`v1.0/fr/guide.md`), so a configured version segment is skipped first.
244
264
  const parts = page.source.ref.split("/");
245
- const first = (
246
- parts[0] && versionDirs.has(parts[0]) ? parts[1] : parts[0]
247
- )?.toLowerCase();
265
+ const folder = parts[0] && versionDirs.has(parts[0]) ? parts[1] : parts[0];
266
+ const first = folder?.toLowerCase();
267
+ if (
268
+ folder !== undefined &&
269
+ first === i18n.defaultLocale.toLowerCase() &&
270
+ !seen.has(first)
271
+ ) {
272
+ seen.add(first);
273
+ diagnostics.push(defaultLocaleFolderDiagnostic(folder, i18n));
274
+ continue;
275
+ }
248
276
  if (
249
277
  first &&
250
278
  !seen.has(first) &&
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
2
3
 
3
- import { relative } from "pathe";
4
+ import { normalize, relative } from "pathe";
4
5
 
5
6
  import type { Diagnostic } from "./types.ts";
6
7
 
@@ -58,6 +59,23 @@ export const parseGitLog = (output: string): Map<string, string> => {
58
59
  return times;
59
60
  };
60
61
 
62
+ /**
63
+ * `path` with every symlink resolved, the spelling git prints paths in: a
64
+ * project opened through a link (`/tmp/site` on macOS, where `/tmp` links to
65
+ * `/private/tmp`) gets `/private/tmp/site` from `rev-parse --show-toplevel`,
66
+ * so the project's own spelling would compare as outside the repository and
67
+ * every page would go undated. The native realpath also expands a Windows
68
+ * 8.3 short name (`RUNNER~1`), which git does too. A path that doesn't exist
69
+ * stays as written.
70
+ */
71
+ export const realPath = (path: string): string => {
72
+ try {
73
+ return normalize(realpathSync.native(path));
74
+ } catch {
75
+ return path;
76
+ }
77
+ };
78
+
61
79
  /**
62
80
  * The toplevel of the repository containing `root`, or null when git is
63
81
  * unavailable or the project isn't a repo. Callers use it to decide which
@@ -108,6 +126,10 @@ export const gitLastModifiedTimes = (
108
126
  if (gitRoot === null) {
109
127
  return new Map();
110
128
  }
129
+ // Git names paths by their real location, so the pathspecs and the paths
130
+ // read back against the log are compared in that spelling too (see
131
+ // `realPath`).
132
+ const top = realPath(gitRoot);
111
133
  try {
112
134
  const output = execFileSync(
113
135
  // oxlint-disable-next-line sonarjs/no-os-command-from-path -- git is a required dev-tool dependency resolved from PATH
@@ -121,14 +143,14 @@ export const gitLastModifiedTimes = (
121
143
  "--format=%x00%cI",
122
144
  "--name-only",
123
145
  "--",
124
- ...contentRoots,
146
+ ...contentRoots.map(realPath),
125
147
  ],
126
148
  { encoding: "utf-8", env: gitEnv(), maxBuffer: 256 * 1024 * 1024 }
127
149
  );
128
150
  const byRepoPath = parseGitLog(output);
129
151
  const result = new Map<string, string>();
130
152
  for (const sourcePath of sourcePaths) {
131
- const iso = byRepoPath.get(relative(gitRoot, sourcePath));
153
+ const iso = byRepoPath.get(relative(top, realPath(sourcePath)));
132
154
  if (iso) {
133
155
  result.set(sourcePath, iso);
134
156
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  isInternalPath,
3
+ mountBasePath,
3
4
  normalizePath,
4
5
  stripBasePath,
5
6
  withBasePath,
@@ -120,7 +121,10 @@ export const localizeHref = (
120
121
  if (localized === based) {
121
122
  return href;
122
123
  }
123
- return `${withBasePath(options.deployBase, localized)}${suffix}`;
124
+ // The base came off above, so it goes back on unconditionally: a localized
125
+ // path that starts with the base's own name (`/ja/…` under base `/ja`) is
126
+ // still served under it.
127
+ return `${mountBasePath(options.deployBase, localized)}${suffix}`;
124
128
  };
125
129
 
126
130
  /** Every `<a …>` opening tag; `\s` keeps `<abbr>`/`<astro-island>` out. */
package/src/core/meta.ts CHANGED
@@ -89,6 +89,38 @@ const withinInclude = (
89
89
  (pattern) => !pattern.startsWith("!") && onPathOf(dir, globBase(pattern))
90
90
  );
91
91
 
92
+ /**
93
+ * The folder-meta files a filesystem source contributes: `patterns` (every
94
+ * meta module name, shared `meta.$.*` included, by default) under the source's
95
+ * root, minus its `exclude` globs, dependencies, and build output, and only in
96
+ * folders its `include` globs reach. `blume translate` finds the meta files it
97
+ * translates through this too, so it covers exactly the ones the scan reads.
98
+ */
99
+ export const findFolderMetaFiles = async (
100
+ source: FolderMetaSource,
101
+ patterns: readonly string[] = META_FILES
102
+ ): Promise<string[]> => {
103
+ const found = await glob([...patterns], {
104
+ absolute: true,
105
+ cwd: source.root,
106
+ // Never descend into dependencies or build output — relevant when the
107
+ // root is the project root (e.g. a `.`-rooted or all-staged project).
108
+ // The source's own `exclude` applies too: a `meta.ts` under an excluded
109
+ // folder (`src/lib/meta.ts` beside `exclude: ["src/**"]`) is application
110
+ // code, not folder meta, and importing it would fail.
111
+ ignore: [
112
+ ...(source.exclude ?? []),
113
+ "**/node_modules/**",
114
+ "**/.blume/**",
115
+ "**/dist/**",
116
+ ],
117
+ onlyFiles: true,
118
+ });
119
+ return found.filter((file) =>
120
+ withinInclude(relative(source.root, dirname(file)), source.include)
121
+ );
122
+ };
123
+
92
124
  /**
93
125
  * The folder-meta key for a directory. Mirrors the sidebar group path: the
94
126
  * source's route prefix (`docs`) followed by the directory relative to the
@@ -155,25 +187,7 @@ export const discoverFolderMeta = async (
155
187
  // `content.root` still contributes its folder meta.
156
188
  const perSource = await Promise.all(
157
189
  list.map(async (source) => {
158
- const found = await glob(META_FILES, {
159
- absolute: true,
160
- cwd: source.root,
161
- // Never descend into dependencies or build output — relevant when the
162
- // root is the project root (e.g. a `.`-rooted or all-staged project).
163
- // The source's own `exclude` applies too: a `meta.ts` under an
164
- // excluded folder (`src/lib/meta.ts` beside `exclude: ["src/**"]`)
165
- // is application code, not folder meta, and importing it would fail.
166
- ignore: [
167
- ...(source.exclude ?? []),
168
- "**/node_modules/**",
169
- "**/.blume/**",
170
- "**/dist/**",
171
- ],
172
- onlyFiles: true,
173
- });
174
- const files = found.filter((file) =>
175
- withinInclude(relative(source.root, dirname(file)), source.include)
176
- );
190
+ const files = await findFolderMetaFiles(source);
177
191
  const loaded = await Promise.all(
178
192
  files.map(
179
193
  async (
@@ -115,11 +115,29 @@ const numericOrder = (segment: string): number => {
115
115
  return value ? Math.trunc(Number(value)) : Number.POSITIVE_INFINITY;
116
116
  };
117
117
 
118
+ /**
119
+ * The name inside a group folder's parentheses — `(guides)`, or `01-(guides)`
120
+ * with its ordering prefix outside them — else undefined. A prefix written
121
+ * inside (`(01-guides)`) stays in the name, where it sorts and drops like any
122
+ * folder's.
123
+ */
124
+ const groupName = (raw: string): string | undefined =>
125
+ (raw.match(GROUP_FOLDER) ?? stripOrderingPrefix(raw).match(GROUP_FOLDER))
126
+ ?.groups?.label;
127
+
128
+ /**
129
+ * A folder's sidebar order: its ordering prefix, or — for a group folder
130
+ * without one outside the parentheses — the prefix inside them, so
131
+ * `01-(guides)` and `(01-guides)` both sort like `01-guides`.
132
+ */
133
+ const folderOrder = (raw: string): number =>
134
+ numericOrder(
135
+ orderingPrefix(raw) === undefined ? (groupName(raw) ?? raw) : raw
136
+ );
137
+
118
138
  /** The nav key of a raw path segment: group label or numeric-stripped name. */
119
- const segmentKey = (raw: string): string => {
120
- const group = raw.match(GROUP_FOLDER)?.groups?.label;
121
- return stripOrderingPrefix(group ?? raw);
122
- };
139
+ const segmentKey = (raw: string): string =>
140
+ stripOrderingPrefix(groupName(raw) ?? raw);
123
141
 
124
142
  /**
125
143
  * Whether a filename stem is a directory index, ignoring an ordering prefix:
@@ -221,8 +239,8 @@ const ensureGroup = (
221
239
  const group = createGroup(
222
240
  segmentKey(rawSegment),
223
241
  path,
224
- humanize(rawSegment.match(GROUP_FOLDER)?.groups?.label ?? rawSegment),
225
- numericOrder(rawSegment)
242
+ humanize(groupName(rawSegment) ?? rawSegment),
243
+ folderOrder(rawSegment)
226
244
  );
227
245
  parent.index.set(rawSegment, group);
228
246
  parent.children.push(group);
@@ -722,7 +740,9 @@ const buildFileSystemSidebar = (
722
740
  const folderParts = isIndexStem(stem)
723
741
  ? routeSegments
724
742
  : routeSegments.slice(0, -1);
725
- const routeDirCount = dirs.filter((dir) => !GROUP_FOLDER.test(dir)).length;
743
+ const routeDirCount = dirs.filter(
744
+ (dir) => groupName(dir) === undefined
745
+ ).length;
726
746
  const offset = Math.max(0, folderParts.length - routeDirCount);
727
747
  // A frontmatter `slug` publishes the page away from its folder, so its
728
748
  // route says nothing about where the folder lives (`guides/a.md` with
@@ -744,7 +764,7 @@ const buildFileSystemSidebar = (
744
764
  const folders: string[] = [];
745
765
  for (const dir of dirs) {
746
766
  parent = ensureGroup(parent, dir);
747
- if (!GROUP_FOLDER.test(dir)) {
767
+ if (groupName(dir) === undefined) {
748
768
  consumed += 1;
749
769
  folders.push(stripOrderingPrefix(dir));
750
770
  }
@@ -3,6 +3,7 @@ import { isAbsolute, relative } from "pathe";
3
3
  import { CHANGELOG_INDEX_ROUTE, hasChangelogIndex } from "./changelog-index.ts";
4
4
  import { loadConfig } from "./config.ts";
5
5
  import { customStaticRoutes, discoverPages } from "./custom-pages.ts";
6
+ import { unknownDirectiveDiagnostics } from "./directive-diagnostics.ts";
6
7
  import { buildContentGraph } from "./graph.ts";
7
8
  import { i18nDiagnostics } from "./i18n.ts";
8
9
  import { expandIncludes, hasIncludeStatements } from "./includes.ts";
@@ -10,6 +11,7 @@ import {
10
11
  gitLastModifiedTimes,
11
12
  gitRepositoryRoot,
12
13
  lastModifiedShallowWarning,
14
+ realPath,
13
15
  resolveLastModifiedConfig,
14
16
  } from "./last-modified.ts";
15
17
  import { buildManifest } from "./manifest.ts";
@@ -196,11 +198,34 @@ const normalizeLoadedEntries = (
196
198
  }
197
199
  pages.push(...normalized.pages);
198
200
  allDiagnostics.push(...normalized.diagnostics);
201
+ if (normalized.pages.length > 0) {
202
+ allDiagnostics.push(...unknownDirectiveDiagnostics(entry, source.name));
203
+ }
199
204
  }
200
205
  }
201
206
  return { diagnostics: allDiagnostics, droppedPages, pages };
202
207
  };
203
208
 
209
+ /**
210
+ * The local sources' on-disk roots a `git log` pathspec can cover: those
211
+ * inside the repository at `gitRoot`. A root outside it would fail the log
212
+ * outright and can never yield dates. Compared by real path, since git spells
213
+ * its toplevel with every symlink resolved (`/private/tmp/site` for a project
214
+ * opened as `/tmp/site`); the roots themselves keep the project's spelling.
215
+ */
216
+ const gitContentRoots = (
217
+ sources: readonly ContentSource[],
218
+ gitRoot: string | null
219
+ ): string[] => {
220
+ if (gitRoot === null) {
221
+ return [];
222
+ }
223
+ const top = realPath(gitRoot);
224
+ return sources
225
+ .flatMap((source) => (source.contentRoot ? [source.contentRoot] : []))
226
+ .filter((dir) => isWithin(top, realPath(dir)));
227
+ };
228
+
204
229
  /**
205
230
  * Run the full core pipeline for a project root: load config, resolve paths,
206
231
  * discover content and folder meta, build the graph, and assemble the manifest.
@@ -362,12 +387,9 @@ export const scanProject = async (
362
387
  // The git pathspecs must cover where the pages actually live: each local
363
388
  // source's own on-disk root — a filesystem source's `root`, or a staged
364
389
  // local source's tree (an Obsidian vault) — which diverges from the global
365
- // `content.root`. A root outside the repository would fail the log outright
366
- // and can never yield dates, so those are dropped up front.
390
+ // `content.root`.
367
391
  const gitRoot = gitRepositoryRoot(context.root);
368
- const contentRoots = sources
369
- .flatMap((source) => (source.contentRoot ? [source.contentRoot] : []))
370
- .filter((dir) => gitRoot !== null && isWithin(gitRoot, dir));
392
+ const contentRoots = gitContentRoots(sources, gitRoot);
371
393
  const gitTimes = gitLastModifiedTimes(
372
394
  context.root,
373
395
  contentRoots,
@@ -1303,9 +1303,9 @@ const ogConfigSchema = z.strictObject({
1303
1303
  */
1304
1304
  enabled: z.boolean().optional(),
1305
1305
  /**
1306
- * Google Font families for the generated card, extending Takumi's Latin-only
1307
- * default so non-Latin titles (CJK, and so on) render instead of tofu.
1308
- * Fetched from Google Fonts at build.
1306
+ * Fonts for the generated card, replacing the default stack (Takumi's
1307
+ * Latin-only built-in font plus a Noto fallback per script). Google
1308
+ * families are fetched from Google Fonts at build.
1309
1309
  */
1310
1310
  fonts: z.array(ogFontSchema).optional(),
1311
1311
  /**
@@ -1687,6 +1687,20 @@ const DATE_FORM_FIELDS = [
1687
1687
  "day",
1688
1688
  ] as const;
1689
1689
 
1690
+ /**
1691
+ * Whether `Intl` knows a time zone. An unknown one (`Asia/Tokio`) throws a
1692
+ * RangeError the first time a date is formatted, deep in a page render, so it
1693
+ * is caught here where the error can point at `dateFormat.timeZone`.
1694
+ */
1695
+ const isKnownTimeZone = (timeZone: string): boolean => {
1696
+ try {
1697
+ Intl.DateTimeFormat("en", { timeZone });
1698
+ return true;
1699
+ } catch {
1700
+ return false;
1701
+ }
1702
+ };
1703
+
1690
1704
  const dateFormatConfigSchema = z
1691
1705
  .strictObject({
1692
1706
  /** Calendar system (e.g. `japanese`, `buddhist`). */
@@ -1702,7 +1716,13 @@ const dateFormatConfigSchema = z
1702
1716
  /** Numbering system (e.g. `latn`, `arab`). */
1703
1717
  numberingSystem: z.string().optional(),
1704
1718
  /** IANA time zone (e.g. `Asia/Tokyo`). Defaults to `UTC`. */
1705
- timeZone: z.string().optional(),
1719
+ timeZone: z
1720
+ .string()
1721
+ .refine(isKnownTimeZone, {
1722
+ message:
1723
+ 'Unknown time zone; use an IANA name like "Asia/Tokyo" or "UTC".',
1724
+ })
1725
+ .optional(),
1706
1726
  /** Weekday representation. */
1707
1727
  weekday: z.enum(["long", "short", "narrow"]).optional(),
1708
1728
  /** Year representation. */