blume 2.0.1 → 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 (492) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/cli/{chunk-qwsrynx5.js → chunk-1d7ve1dm.js} +38 -19
  3. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-1d7ve1dm.js.map} +4 -4
  4. package/dist/cli/{chunk-s6jhgk0q.js → chunk-2eytanqx.js} +2 -2
  5. package/dist/cli/{chunk-kdp5q7ke.js → chunk-2hsdwb9n.js} +18 -19
  6. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-2hsdwb9n.js.map} +2 -2
  7. package/dist/cli/{chunk-hdpx1tax.js → chunk-2z928egk.js} +5 -5
  8. package/dist/cli/{chunk-jwyddg7y.js → chunk-35d4wj9f.js} +30 -24
  9. package/dist/cli/chunk-35d4wj9f.js.map +15 -0
  10. package/dist/cli/{chunk-ah61y8py.js → chunk-364znk6q.js} +2 -2
  11. package/dist/cli/{chunk-jts8mvcz.js → chunk-3em5wd2y.js} +25 -6
  12. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-3em5wd2y.js.map} +3 -3
  13. package/dist/cli/{chunk-s1p84fyh.js → chunk-5f86nr5m.js} +63 -22
  14. package/dist/cli/chunk-5f86nr5m.js.map +11 -0
  15. package/dist/cli/{chunk-2hn4b8z7.js → chunk-5m5nmvyq.js} +170 -76
  16. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  17. package/dist/cli/{chunk-epjnccmv.js → chunk-5xvm6tfj.js} +18 -15
  18. package/dist/cli/chunk-5xvm6tfj.js.map +10 -0
  19. package/dist/cli/{chunk-fz5wtpmh.js → chunk-6dsbexzp.js} +18 -15
  20. package/dist/cli/chunk-6dsbexzp.js.map +10 -0
  21. package/dist/cli/{chunk-vtk4a6dg.js → chunk-8ktnccpt.js} +1 -1
  22. package/dist/cli/{chunk-6hsn950k.js → chunk-9t7a85s3.js} +178 -48
  23. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  24. package/dist/cli/{chunk-fxypxtvm.js → chunk-a58773jm.js} +2 -2
  25. package/dist/cli/{chunk-mb2919y2.js → chunk-acanzt5p.js} +17 -6
  26. package/dist/cli/chunk-acanzt5p.js.map +10 -0
  27. package/dist/cli/{chunk-27g6wdth.js → chunk-akbpwfxc.js} +90 -26
  28. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  29. package/dist/cli/{chunk-wm7js3j9.js → chunk-b07cmahc.js} +2 -2
  30. package/dist/cli/{chunk-wgm7m9qk.js → chunk-c8chx29p.js} +179 -51
  31. package/dist/cli/chunk-c8chx29p.js.map +36 -0
  32. package/dist/cli/{chunk-qs4q5p4e.js → chunk-crgn1q09.js} +32 -14
  33. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  34. package/dist/cli/chunk-e04dxsz1.js +39 -0
  35. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  36. package/dist/cli/{chunk-yt5n7ppj.js → chunk-ey84smr6.js} +17 -7
  37. package/dist/cli/chunk-ey84smr6.js.map +10 -0
  38. package/dist/cli/{chunk-f2z5v128.js → chunk-g4hq16wv.js} +14 -15
  39. package/dist/cli/{chunk-f2z5v128.js.map → chunk-g4hq16wv.js.map} +2 -2
  40. package/dist/cli/{chunk-fa25z98p.js → chunk-ga0pf4aj.js} +9 -8
  41. package/dist/cli/chunk-ga0pf4aj.js.map +11 -0
  42. package/dist/cli/{chunk-kpf8rrjc.js → chunk-hm3vjy5s.js} +109 -58
  43. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  44. package/dist/cli/{chunk-fs23ddbb.js → chunk-j85vccga.js} +625 -750
  45. package/dist/cli/chunk-j85vccga.js.map +36 -0
  46. package/dist/cli/{chunk-ch6g3ar0.js → chunk-p3v96n38.js} +6 -6
  47. package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-p3v96n38.js.map} +3 -3
  48. package/dist/cli/{chunk-q5163e60.js → chunk-p73c0m7w.js} +21 -19
  49. package/dist/cli/chunk-p73c0m7w.js.map +11 -0
  50. package/dist/cli/{chunk-dh8cwk36.js → chunk-pehfxfta.js} +24 -9
  51. package/dist/cli/chunk-pehfxfta.js.map +10 -0
  52. package/dist/cli/{chunk-qkqwkpte.js → chunk-pv29h0wf.js} +3688 -1313
  53. package/dist/cli/chunk-pv29h0wf.js.map +190 -0
  54. package/dist/cli/{chunk-6vm74dry.js → chunk-q58y5e6a.js} +10 -10
  55. package/dist/cli/{chunk-6vm74dry.js.map → chunk-q58y5e6a.js.map} +3 -3
  56. package/dist/cli/{chunk-zxcczpyx.js → chunk-r20tn01b.js} +1 -1
  57. package/dist/cli/{chunk-m3vmjgmq.js → chunk-r9rcc4w7.js} +16 -8
  58. package/dist/cli/chunk-r9rcc4w7.js.map +10 -0
  59. package/dist/cli/{chunk-5shv93fd.js → chunk-tkacnehg.js} +2 -2
  60. package/dist/cli/{chunk-zxh4d9vy.js → chunk-tzmab476.js} +4 -4
  61. package/dist/cli/{chunk-yw7dm696.js → chunk-vg9r4eb9.js} +24 -11
  62. package/dist/cli/chunk-vg9r4eb9.js.map +14 -0
  63. package/dist/cli/{chunk-79jhk4py.js → chunk-wrr3j9w9.js} +260 -132
  64. package/dist/cli/chunk-wrr3j9w9.js.map +35 -0
  65. package/dist/cli/{chunk-6crbhc3x.js → chunk-yfyb25rh.js} +54 -27
  66. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  67. package/dist/cli/index.js +162 -35
  68. package/dist/cli/index.js.map +4 -4
  69. package/dist/types/ai/agent-surface.d.ts +32 -0
  70. package/dist/types/ai/api-catalog.d.ts +7 -1
  71. package/dist/types/ai/ask-context.d.ts +7 -0
  72. package/dist/types/ai/component-markdown.d.ts +4 -4
  73. package/dist/types/ai/link-headers.d.ts +8 -1
  74. package/dist/types/ai/openapi-components.d.ts +5 -2
  75. package/dist/types/ai/relative-links.d.ts +11 -4
  76. package/dist/types/ai/skills.d.ts +4 -1
  77. package/dist/types/ai/static-expression.d.ts +28 -0
  78. package/dist/types/ai/tar.d.ts +1 -3
  79. package/dist/types/analytics/databuddy.d.ts +43 -0
  80. package/dist/types/analytics/index.d.ts +4 -0
  81. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  82. package/dist/types/analytics/schema.d.ts +28 -0
  83. package/dist/types/astro/integration.d.ts +3 -2
  84. package/dist/types/cli/env.d.ts +5 -0
  85. package/dist/types/cli/init/scaffold.d.ts +19 -3
  86. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  87. package/dist/types/core/base-path.d.ts +24 -1
  88. package/dist/types/core/config-input.d.ts +13 -11
  89. package/dist/types/core/config.d.ts +2 -2
  90. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  91. package/dist/types/core/graph.d.ts +2 -0
  92. package/dist/types/core/heading-markers.d.ts +5 -7
  93. package/dist/types/core/i18n-ui.d.ts +31 -0
  94. package/dist/types/core/i18n.d.ts +9 -1
  95. package/dist/types/core/last-modified.d.ts +10 -0
  96. package/dist/types/core/links.d.ts +3 -1
  97. package/dist/types/core/load-module.d.ts +10 -0
  98. package/dist/types/core/locale-links.d.ts +12 -2
  99. package/dist/types/core/meta.d.ts +13 -1
  100. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  101. package/dist/types/core/navigation.d.ts +38 -0
  102. package/dist/types/core/ordering-prefix.d.ts +4 -0
  103. package/dist/types/core/safe-links.d.ts +3 -1
  104. package/dist/types/core/schema.d.ts +59 -13
  105. package/dist/types/core/sources/github-releases.d.ts +5 -0
  106. package/dist/types/core/sources/lower.d.ts +38 -13
  107. package/dist/types/core/sources/normalize.d.ts +23 -2
  108. package/dist/types/core/sources/remote.d.ts +11 -1
  109. package/dist/types/core/sources/resolve.d.ts +12 -0
  110. package/dist/types/core/sources/types.d.ts +31 -0
  111. package/dist/types/core/sources/watch.d.ts +12 -5
  112. package/dist/types/core/standard-schema.d.ts +5 -0
  113. package/dist/types/core/types.d.ts +9 -0
  114. package/dist/types/deploy/adapters/node.d.ts +5 -2
  115. package/dist/types/deploy/adapters/types.d.ts +7 -0
  116. package/dist/types/deploy/artifacts.d.ts +6 -4
  117. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  118. package/dist/types/deploy/headers.d.ts +36 -7
  119. package/dist/types/deploy/node-headers.d.ts +43 -8
  120. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  121. package/dist/types/deploy/platforms/node.d.ts +6 -5
  122. package/dist/types/deploy/platforms/types.d.ts +15 -0
  123. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  124. package/dist/types/deploy/redirects.d.ts +31 -15
  125. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  126. package/dist/types/markdown/directives.d.ts +62 -0
  127. package/dist/types/markdown/features.d.ts +21 -0
  128. package/dist/types/markdown/mdast.d.ts +63 -0
  129. package/dist/types/openapi/asyncapi.d.ts +4 -2
  130. package/dist/types/openapi/model.d.ts +20 -6
  131. package/dist/types/search/sync/algolia.d.ts +3 -1
  132. package/docs/01-quickstart.mdx +3 -2
  133. package/docs/02-deployment.mdx +20 -9
  134. package/docs/08-faq.mdx +10 -3
  135. package/docs/advanced/changelog.mdx +1 -1
  136. package/docs/advanced/custom-pages.mdx +7 -5
  137. package/docs/cli/audit.mdx +19 -3
  138. package/docs/cli/doctor.mdx +2 -2
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/index.mdx +4 -1
  141. package/docs/cli/translate.mdx +4 -4
  142. package/docs/cli/version.mdx +1 -1
  143. package/docs/configuration/analytics.mdx +42 -2
  144. package/docs/configuration/assistant.mdx +1 -1
  145. package/docs/configuration/customization.mdx +6 -3
  146. package/docs/configuration/index.mdx +5 -3
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +2 -2
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -1
  151. package/docs/content/index.mdx +8 -4
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +7 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -16
  156. package/docs/content/syntax.mdx +14 -0
  157. package/docs/content/versioning.mdx +2 -1
  158. package/docs/discoverability/agent-discovery.mdx +22 -8
  159. package/docs/discoverability/index.mdx +4 -3
  160. package/docs/discoverability/llms-txt.mdx +2 -5
  161. package/docs/discoverability/markdown.mdx +6 -4
  162. package/docs/discoverability/mcp.mdx +4 -0
  163. package/docs/discoverability/metadata.mdx +3 -2
  164. package/docs/discoverability/open-graph.mdx +8 -4
  165. package/docs/discoverability/rss.mdx +4 -2
  166. package/docs/references/asyncapi.mdx +2 -2
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +6 -4
  169. package/package.json +1 -1
  170. package/skills/blume-migrate/SKILL.md +6 -6
  171. package/skills/blume-migrate/references/docusaurus.md +6 -6
  172. package/skills/blume-migrate/references/fumadocs.md +1 -1
  173. package/skills/blume-migrate/references/mintlify.md +3 -3
  174. package/skills/blume-migrate/references/monorepo.md +1 -1
  175. package/skills/blume-migrate/references/nextra.md +2 -2
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +9 -4
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +2 -2
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/ai-catalog.ts +6 -2
  182. package/src/ai/api/handlers.ts +2 -2
  183. package/src/ai/api/spec.ts +17 -4
  184. package/src/ai/api-catalog.ts +13 -3
  185. package/src/ai/ask-context.ts +14 -2
  186. package/src/ai/ask-data.ts +26 -12
  187. package/src/ai/changelog-markdown.ts +2 -2
  188. package/src/ai/component-markdown.ts +43 -37
  189. package/src/ai/link-headers.ts +22 -8
  190. package/src/ai/llms.ts +22 -10
  191. package/src/ai/markdown.ts +23 -11
  192. package/src/ai/mcp/discovery.ts +2 -2
  193. package/src/ai/mcp/query.ts +2 -2
  194. package/src/ai/mcp/server.ts +80 -9
  195. package/src/ai/openapi-components.ts +22 -6
  196. package/src/ai/relative-links.ts +99 -15
  197. package/src/ai/serializers.ts +2 -1
  198. package/src/ai/skills.ts +14 -1
  199. package/src/ai/static-expression.ts +416 -0
  200. package/src/ai/tar.ts +139 -12
  201. package/src/ai/visibility.ts +45 -14
  202. package/src/analytics/databuddy.ts +67 -0
  203. package/src/analytics/head.ts +8 -0
  204. package/src/analytics/index.ts +7 -0
  205. package/src/analytics/one-dollar-stats.ts +86 -0
  206. package/src/analytics/posthog.ts +21 -3
  207. package/src/analytics/schema.ts +4 -0
  208. package/src/astro/generate.ts +72 -28
  209. package/src/astro/include-hmr.ts +31 -12
  210. package/src/astro/include-refresh.ts +19 -8
  211. package/src/astro/integration.ts +108 -14
  212. package/src/astro/runtime-modules.ts +28 -13
  213. package/src/astro/templates.ts +139 -57
  214. package/src/audit/catalog.ts +2 -2
  215. package/src/audit/checks/assets.ts +25 -5
  216. package/src/audit/checks/content.ts +20 -2
  217. package/src/audit/checks/i18n.ts +46 -8
  218. package/src/audit/checks/indexability.ts +39 -19
  219. package/src/audit/checks/links.ts +22 -1
  220. package/src/audit/checks/llms.ts +6 -3
  221. package/src/audit/checks/network.ts +3 -1
  222. package/src/audit/checks/og-image.ts +10 -0
  223. package/src/audit/checks/robots.ts +6 -1
  224. package/src/audit/checks/sitemap.ts +56 -31
  225. package/src/audit/checks/social.ts +21 -2
  226. package/src/audit/crawl.ts +88 -15
  227. package/src/audit/graph.ts +3 -1
  228. package/src/audit/report.ts +54 -17
  229. package/src/audit/run.ts +13 -9
  230. package/src/audit/snapshot.ts +5 -0
  231. package/src/audit/types.ts +18 -0
  232. package/src/audit/url.ts +37 -6
  233. package/src/cli/args.ts +32 -0
  234. package/src/cli/build-failure.ts +50 -0
  235. package/src/cli/commands/audit.ts +11 -1
  236. package/src/cli/commands/build.ts +19 -5
  237. package/src/cli/commands/check.ts +2 -0
  238. package/src/cli/commands/doctor.ts +3 -1
  239. package/src/cli/commands/eval.ts +20 -18
  240. package/src/cli/commands/init.ts +15 -5
  241. package/src/cli/commands/preview.ts +15 -0
  242. package/src/cli/commands/sync.ts +2 -0
  243. package/src/cli/commands/translate.ts +18 -18
  244. package/src/cli/commands/upgrade.ts +11 -0
  245. package/src/cli/commands/validate.ts +3 -1
  246. package/src/cli/dev-lock.ts +157 -37
  247. package/src/cli/eject-scripts.ts +32 -7
  248. package/src/cli/env.ts +12 -1
  249. package/src/cli/init/scaffold.ts +117 -11
  250. package/src/cli/init/starter-spec.ts +235 -0
  251. package/src/cli/report-format.ts +11 -6
  252. package/src/components/colors.ts +19 -0
  253. package/src/components/content/AccordionItem.astro +26 -22
  254. package/src/components/content/Badge.astro +7 -12
  255. package/src/components/content/Card.astro +2 -2
  256. package/src/components/content/Component.astro +11 -5
  257. package/src/components/content/Expandable.astro +5 -1
  258. package/src/components/content/Frame.astro +2 -7
  259. package/src/components/content/GithubInfo.astro +12 -2
  260. package/src/components/content/Prompt.astro +2 -7
  261. package/src/components/content/Tab.astro +0 -1
  262. package/src/components/content/Tabs.astro +58 -5
  263. package/src/components/content/Tile.astro +1 -1
  264. package/src/components/content/Tooltip.astro +69 -7
  265. package/src/components/content/Tree.astro +7 -2
  266. package/src/components/content/TypeTable.astro +10 -5
  267. package/src/components/content/Update.astro +8 -2
  268. package/src/components/content/auto-type-table.ts +4 -1
  269. package/src/components/content/badge-color.ts +20 -0
  270. package/src/components/content/base-href.ts +14 -22
  271. package/src/components/content/inline-markdown.ts +27 -7
  272. package/src/components/copy-feedback.ts +35 -8
  273. package/src/components/islands/assistant.tsx +37 -6
  274. package/src/components/islands/base-path.ts +47 -10
  275. package/src/components/islands/hooks.ts +42 -21
  276. package/src/components/islands/webmcp.ts +16 -10
  277. package/src/components/layout/Banner.astro +23 -4
  278. package/src/components/layout/Breadcrumbs.astro +5 -2
  279. package/src/components/layout/DiscoveryLinks.astro +9 -5
  280. package/src/components/layout/Header.astro +23 -10
  281. package/src/components/layout/LanguageSwitcher.astro +2 -2
  282. package/src/components/layout/Logo.astro +5 -0
  283. package/src/components/layout/NavSelector.astro +8 -3
  284. package/src/components/layout/NavTabMenu.astro +133 -0
  285. package/src/components/layout/NavTree.astro +31 -12
  286. package/src/components/layout/NavTreeCache.astro +5 -2
  287. package/src/components/layout/NavTreeScript.astro +45 -5
  288. package/src/components/layout/PageActions.astro +4 -3
  289. package/src/components/layout/PageLayout.astro +28 -13
  290. package/src/components/layout/Pagination.astro +3 -3
  291. package/src/components/layout/ReferenceLayout.astro +4 -1
  292. package/src/components/layout/RootLayout.astro +25 -12
  293. package/src/components/layout/Search.astro +38 -14
  294. package/src/components/layout/VersionBanner.astro +2 -2
  295. package/src/components/layout/analytics-client.ts +14 -0
  296. package/src/components/layout/toc-active.ts +41 -0
  297. package/src/components/layout/toc-element.ts +8 -14
  298. package/src/components/openapi/ApiTagOperations.astro +2 -2
  299. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  300. package/src/components/openapi/GraphqlChip.astro +2 -2
  301. package/src/components/openapi/GraphqlType.astro +11 -3
  302. package/src/components/openapi/MessageComposer.astro +1 -1
  303. package/src/components/openapi/Operation.astro +23 -5
  304. package/src/components/openapi/PanelTabs.astro +4 -1
  305. package/src/components/openapi/Playground.astro +8 -2
  306. package/src/components/openapi/RequestPanel.astro +13 -6
  307. package/src/components/openapi/SchemaProperty.astro +11 -33
  308. package/src/components/openapi/SchemaTable.astro +34 -52
  309. package/src/components/openapi/async.ts +38 -6
  310. package/src/components/openapi/helpers.ts +100 -12
  311. package/src/components/openapi/message-composer.ts +14 -1
  312. package/src/components/openapi/message-model.ts +12 -2
  313. package/src/components/openapi/message.ts +21 -3
  314. package/src/components/openapi/operation-model.ts +119 -17
  315. package/src/components/openapi/panel.ts +31 -5
  316. package/src/components/openapi/param-style.ts +181 -0
  317. package/src/components/openapi/playground-client.ts +110 -23
  318. package/src/components/openapi/playground-schema.ts +25 -6
  319. package/src/components/openapi/request.ts +192 -26
  320. package/src/components/openapi/schema-tree.ts +209 -0
  321. package/src/components/openapi/snippets.ts +113 -19
  322. package/src/components/openapi/validate-json.ts +1 -1
  323. package/src/components/openapi/ws-client.ts +18 -2
  324. package/src/core/base-path.ts +69 -8
  325. package/src/core/config-input.ts +13 -11
  326. package/src/core/config.ts +18 -4
  327. package/src/core/diagnostics.ts +219 -30
  328. package/src/core/directive-diagnostics.ts +99 -0
  329. package/src/core/frontmatter.ts +21 -18
  330. package/src/core/graph.ts +120 -11
  331. package/src/core/heading-markers.ts +5 -18
  332. package/src/core/i18n-ui.ts +39 -2
  333. package/src/core/i18n.ts +44 -5
  334. package/src/core/last-modified.ts +25 -3
  335. package/src/core/links.ts +7 -4
  336. package/src/core/load-module.ts +20 -0
  337. package/src/core/locale-links.ts +22 -18
  338. package/src/core/manifest.ts +3 -2
  339. package/src/core/meta.ts +89 -12
  340. package/src/core/nav-diagnostics.ts +56 -1
  341. package/src/core/navigation.ts +272 -83
  342. package/src/core/ordering-prefix.ts +27 -0
  343. package/src/core/project-graph.ts +50 -6
  344. package/src/core/safe-href.ts +53 -1
  345. package/src/core/safe-links.ts +11 -2
  346. package/src/core/schema.ts +94 -18
  347. package/src/core/sources/assets.ts +83 -41
  348. package/src/core/sources/contentful-rich-text.ts +28 -20
  349. package/src/core/sources/contentful.ts +25 -12
  350. package/src/core/sources/filesystem.ts +25 -3
  351. package/src/core/sources/github-releases.ts +131 -11
  352. package/src/core/sources/lexical.ts +30 -20
  353. package/src/core/sources/lower.ts +276 -56
  354. package/src/core/sources/mdx-remote.ts +52 -16
  355. package/src/core/sources/normalize.ts +456 -119
  356. package/src/core/sources/notion.ts +77 -34
  357. package/src/core/sources/obsidian.ts +40 -9
  358. package/src/core/sources/payload.ts +1 -0
  359. package/src/core/sources/portable-text.ts +85 -49
  360. package/src/core/sources/remote.ts +18 -2
  361. package/src/core/sources/resolve.ts +52 -33
  362. package/src/core/sources/sanity.ts +1 -0
  363. package/src/core/sources/strapi-blocks.ts +28 -19
  364. package/src/core/sources/strapi.ts +1 -0
  365. package/src/core/sources/types.ts +31 -0
  366. package/src/core/sources/watch.ts +20 -7
  367. package/src/core/standard-schema.ts +10 -6
  368. package/src/core/types.ts +9 -0
  369. package/src/core/ui-packs/ar.ts +13 -0
  370. package/src/core/ui-packs/bg.ts +13 -0
  371. package/src/core/ui-packs/bn.ts +13 -0
  372. package/src/core/ui-packs/ca.ts +13 -0
  373. package/src/core/ui-packs/cs.ts +13 -0
  374. package/src/core/ui-packs/da.ts +13 -0
  375. package/src/core/ui-packs/de.ts +13 -0
  376. package/src/core/ui-packs/el.ts +13 -0
  377. package/src/core/ui-packs/es.ts +13 -0
  378. package/src/core/ui-packs/fa.ts +13 -0
  379. package/src/core/ui-packs/fi.ts +13 -0
  380. package/src/core/ui-packs/fr.ts +13 -0
  381. package/src/core/ui-packs/he.ts +13 -0
  382. package/src/core/ui-packs/hi.ts +13 -0
  383. package/src/core/ui-packs/hr.ts +13 -0
  384. package/src/core/ui-packs/hu.ts +13 -0
  385. package/src/core/ui-packs/id.ts +13 -0
  386. package/src/core/ui-packs/it.ts +13 -0
  387. package/src/core/ui-packs/ja.ts +13 -0
  388. package/src/core/ui-packs/ko.ts +13 -0
  389. package/src/core/ui-packs/nl.ts +13 -0
  390. package/src/core/ui-packs/no.ts +13 -0
  391. package/src/core/ui-packs/pl.ts +13 -0
  392. package/src/core/ui-packs/pt-br.ts +13 -0
  393. package/src/core/ui-packs/pt.ts +13 -0
  394. package/src/core/ui-packs/ro.ts +13 -0
  395. package/src/core/ui-packs/ru.ts +13 -0
  396. package/src/core/ui-packs/sk.ts +13 -0
  397. package/src/core/ui-packs/sr.ts +13 -0
  398. package/src/core/ui-packs/sv.ts +13 -0
  399. package/src/core/ui-packs/th.ts +13 -0
  400. package/src/core/ui-packs/tr.ts +13 -0
  401. package/src/core/ui-packs/uk.ts +13 -0
  402. package/src/core/ui-packs/vi.ts +13 -0
  403. package/src/core/ui-packs/zh-tw.ts +13 -0
  404. package/src/core/ui-packs/zh.ts +13 -0
  405. package/src/core/version-cut.ts +69 -17
  406. package/src/core/versions.ts +4 -1
  407. package/src/deploy/adapters/node.ts +5 -2
  408. package/src/deploy/adapters/registry.ts +2 -1
  409. package/src/deploy/adapters/types.ts +13 -1
  410. package/src/deploy/artifacts.ts +52 -11
  411. package/src/deploy/cloudflare-negotiation.ts +27 -16
  412. package/src/deploy/headers.ts +75 -55
  413. package/src/deploy/node-headers.ts +148 -27
  414. package/src/deploy/platforms/cloudflare.ts +13 -3
  415. package/src/deploy/platforms/netlify.ts +83 -5
  416. package/src/deploy/platforms/node.ts +8 -5
  417. package/src/deploy/platforms/static.ts +2 -0
  418. package/src/deploy/platforms/types.ts +15 -0
  419. package/src/deploy/platforms/vercel.ts +10 -3
  420. package/src/deploy/redirects.ts +68 -21
  421. package/src/deploy/robots.ts +2 -2
  422. package/src/deploy/rss.ts +4 -3
  423. package/src/deploy/sitemap.ts +7 -5
  424. package/src/deploy/vercel-negotiation.ts +35 -3
  425. package/src/eval/agents.ts +10 -2
  426. package/src/eval/run.ts +25 -0
  427. package/src/markdown/base-links.ts +74 -29
  428. package/src/markdown/directives.ts +242 -36
  429. package/src/markdown/features.ts +17 -0
  430. package/src/markdown/index.ts +13 -9
  431. package/src/markdown/mdast.ts +5 -2
  432. package/src/markdown/relative-links.ts +15 -27
  433. package/src/markdown/route-snapshot.ts +37 -0
  434. package/src/og/card.ts +149 -9
  435. package/src/og/derive.ts +156 -5
  436. package/src/og/index.ts +1 -0
  437. package/src/openapi/asyncapi.ts +4 -2
  438. package/src/openapi/graphql-build.ts +28 -2
  439. package/src/openapi/model.ts +99 -27
  440. package/src/openapi/proxy.ts +63 -10
  441. package/src/openapi/render-mdx.ts +51 -3
  442. package/src/registry/eject.ts +297 -43
  443. package/src/search/adapters/version-scope.ts +30 -0
  444. package/src/search/documents.ts +89 -27
  445. package/src/search/popular.ts +2 -1
  446. package/src/search/sync/algolia.ts +36 -2
  447. package/src/seo/jsonld.ts +7 -3
  448. package/src/sources/registry.ts +5 -0
  449. package/src/theme/entry.ts +11 -4
  450. package/src/translate/agents.ts +6 -1
  451. package/src/translate/ledger.ts +26 -3
  452. package/src/translate/meta.ts +76 -24
  453. package/src/translate/run.ts +14 -8
  454. package/src/translate/validate.ts +14 -2
  455. package/src/translate/work-list.ts +91 -19
  456. package/src/upgrade/upgrade.ts +36 -4
  457. package/dist/cli/chunk-27g6wdth.js.map +0 -10
  458. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  459. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  460. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  461. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  462. package/dist/cli/chunk-82bbrxdn.js +0 -51
  463. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  464. package/dist/cli/chunk-abh8yjkn.js +0 -31
  465. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  466. package/dist/cli/chunk-ce574jw2.js +0 -23
  467. package/dist/cli/chunk-ce574jw2.js.map +0 -10
  468. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  469. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  470. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  471. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  472. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  473. package/dist/cli/chunk-jwyddg7y.js.map +0 -15
  474. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  475. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  476. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  477. package/dist/cli/chunk-q5163e60.js.map +0 -11
  478. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  479. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  480. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  481. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  482. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  483. package/dist/cli/chunk-yw7dm696.js.map +0 -14
  484. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-2eytanqx.js.map} +0 -0
  485. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-2z928egk.js.map} +0 -0
  486. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-364znk6q.js.map} +0 -0
  487. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-8ktnccpt.js.map} +0 -0
  488. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-a58773jm.js.map} +0 -0
  489. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-b07cmahc.js.map} +0 -0
  490. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-r20tn01b.js.map} +0 -0
  491. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-tkacnehg.js.map} +0 -0
  492. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-tzmab476.js.map} +0 -0
@@ -1,14 +1,19 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
 
3
3
  import GithubSlugger from "github-slugger";
4
+ import type { Nodes } from "mdast";
4
5
  import { extname } from "pathe";
6
+ import { markdownToMdast } from "satteri";
5
7
 
6
- import { withBasePath } from "../base-path.ts";
8
+ import { MARKDOWN_FEATURES } from "../../markdown/features.ts";
9
+ import { mountBasePath } from "../base-path.ts";
7
10
  import { nextFenceState } from "../code-fences.ts";
8
11
  import type { FenceState } from "../code-fences.ts";
9
12
  import { diagnosticsFromIssues, diagnosticsFromZod } from "../diagnostics.ts";
10
13
  import { occupySlug, parseHeadingMarkers } from "../heading-markers.ts";
11
14
  import { localePlacement, localizeRoute } from "../i18n.ts";
15
+ import { titleWord } from "../navigation.ts";
16
+ import { stripOrderingPrefix } from "../ordering-prefix.ts";
12
17
  import { pageMetaSchema } from "../schema.ts";
13
18
  import type {
14
19
  FrontmatterExtend,
@@ -20,18 +25,25 @@ import type { Diagnostic, Heading, PageLink, PageRecord } from "../types.ts";
20
25
  import { detectVersionRef, versionizeRoute } from "../versions.ts";
21
26
  import type { NormalizeContext, SourceEntry } from "./types.ts";
22
27
 
23
- const NUMERIC_PREFIX = /^\d+[-_.]/u;
24
28
  const GROUP_FOLDER = /^\((?<label>.+)\)$/u;
25
29
  const WORD_SPLIT = /[-_]/u;
26
30
 
27
31
  /** Strip a leading numeric ordering prefix (`01-intro` -> `intro`). */
28
32
  const stripNumericPrefix = (segment: string): string =>
29
- segment.replace(NUMERIC_PREFIX, "");
33
+ stripOrderingPrefix(segment);
30
34
 
31
35
  /** Detect a group folder `(name)` and return its label, else null. */
32
36
  const groupLabel = (segment: string): string | null =>
33
37
  segment.match(GROUP_FOLDER)?.groups?.label ?? null;
34
38
 
39
+ /**
40
+ * {@link groupLabel} for a file or folder name, which may carry its ordering
41
+ * prefix outside the parentheses: `01-(guides)` is the `guides` group, sorted
42
+ * first, the way `01-guides` is the `guides` folder.
43
+ */
44
+ const orderedGroupLabel = (segment: string): string | null =>
45
+ groupLabel(segment) ?? groupLabel(stripNumericPrefix(segment));
46
+
35
47
  /**
36
48
  * Slugify a content/route slug (Sanity, Notion, frontmatter `slug`). Heading
37
49
  * anchor ids are *not* slugged here — they use a `github-slugger` in
@@ -63,13 +75,9 @@ export const slugify = (text: string): string =>
63
75
  export const slugifyPath = (text: string): string =>
64
76
  text.split("/").map(slugify).filter(Boolean).join("/");
65
77
 
66
- /** Title-case a slug segment for display. */
78
+ /** Title-case a slug segment for display, acronyms as the sidebar spells them. */
67
79
  const titleCase = (value: string): string =>
68
- value
69
- .split(WORD_SPLIT)
70
- .filter(Boolean)
71
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
72
- .join(" ");
80
+ value.split(WORD_SPLIT).filter(Boolean).map(titleWord).join(" ");
73
81
 
74
82
  /**
75
83
  * Strip characters that cannot survive the route → URL → output-file round
@@ -77,29 +85,68 @@ const titleCase = (value: string): string =>
77
85
  * scheme (`Guide: Architecture.md` → `guide:`), which crashes Astro's
78
86
  * prerender write with "The URL must be of scheme file"; control characters
79
87
  * (an embedded newline in a filename) are silently dropped by the URL parser,
80
- * desyncing the route from its output path. Both are legal in macOS/Linux
81
- * filenames, so they are removed here rather than rejected.
88
+ * desyncing the route from its output path. `#` and `?` start a URL's
89
+ * fragment and query, so an unescaped `/sdks/c#` link lands on `/sdks/c`
90
+ * while Astro writes the page to `c%23/`; a `%` starts an escape, and a bare
91
+ * one is an invalid URL Astro can't decode (and one it escapes, `%25`, finds
92
+ * no static path). All are legal in macOS/Linux filenames, so they are
93
+ * removed here rather than rejected: `100%.md` publishes at `/100`, and a
94
+ * slug of `sdks/c#` at `/sdks/c`. A file whose own path holds `#` or `?`
95
+ * never gets this far — Astro can't load it (see
96
+ * {@link unloadablePathDiagnostic}).
82
97
  */
83
98
  const sanitizeSegment = (segment: string): string =>
84
- segment.replaceAll(/[:\p{Cc}]/gu, "");
99
+ segment.replaceAll(/[:#?%\p{Cc}]/gu, "");
100
+
101
+ // Astro's content loader reads each entry at `new URL("./" + encodeURI(entry),
102
+ // base)`. `encodeURI` escapes `%` but leaves `#` and `?` alone, so in a path
103
+ // they start the URL's fragment and query, and the read misses the file.
104
+ const UNLOADABLE_PATH = /[#?]/u;
85
105
 
86
- /** Fold one raw path part into the accumulating route segments/groups. */
106
+ /**
107
+ * The error for a content file Astro's content loader can't read, or
108
+ * undefined when it can. A `#` or `?` anywhere in the path the loader is
109
+ * handed (`sdks/c#.md`, `faq/why?.md`) truncates the file URL it reads
110
+ * through, so the read fails with ENOENT and the page renders "Page not
111
+ * found" at its route. A source leaves such a file out of its scan and
112
+ * reports this instead of publishing a route that can never render.
113
+ */
114
+ export const unloadablePathDiagnostic = (
115
+ path: string,
116
+ file: string
117
+ ): Diagnostic | undefined =>
118
+ UNLOADABLE_PATH.test(path)
119
+ ? {
120
+ code: "BLUME_UNLOADABLE_FILE_NAME",
121
+ file,
122
+ message: `"${path}" has a "#" or "?" in its path, which Astro's content loader reads as the start of a URL fragment or query, so it can't load the file. It was left out of the site.`,
123
+ severity: "error",
124
+ suggestion:
125
+ 'Rename the file (or its folder) without "#" or "?". Both are dropped from the page\'s URL anyway, so the page keeps its route.',
126
+ }
127
+ : undefined;
128
+
129
+ /**
130
+ * Fold one raw path part into the accumulating route segments/groups.
131
+ * `ordered` parts are file or folder names, whose ordering prefix is dropped.
132
+ */
87
133
  const addRouteSegment = (
88
134
  part: string,
89
135
  segments: string[],
90
- groups: string[]
136
+ groups: string[],
137
+ ordered: boolean
91
138
  ): void => {
92
139
  // A leading/trailing/double slash yields an empty part; keeping it would
93
140
  // produce a malformed route (`//foo`, `/foo/`) that nothing can link to.
94
141
  if (part === "") {
95
142
  return;
96
143
  }
97
- const group = groupLabel(part);
144
+ const group = ordered ? orderedGroupLabel(part) : groupLabel(part);
98
145
  if (group !== null) {
99
146
  groups.push(group);
100
147
  return;
101
148
  }
102
- const clean = stripNumericPrefix(part);
149
+ const clean = ordered ? stripNumericPrefix(part) : part;
103
150
  if (clean === "index") {
104
151
  return;
105
152
  }
@@ -119,22 +166,31 @@ interface MappedRoute {
119
166
  }
120
167
 
121
168
  /**
122
- * Convert a content-root-relative path into URL + nav metadata. Not exported:
123
- * a source that needs to predict a route goes through
169
+ * Convert a source's route prefix and a content-root-relative path into URL +
170
+ * nav metadata. Only the path's parts lose an ordering prefix, and only when
171
+ * `ordered` says they are file and folder names: the route prefix, a slug,
172
+ * a release tag, or a CMS slug is a route spelled out, kept as written. Not
173
+ * exported: a source that needs to predict a route goes through
124
174
  * {@link resolveEntryRoute}, so there is exactly one derivation.
125
175
  */
126
- const mapRoute = (relativePath: string): MappedRoute => {
176
+ const mapRoute = (
177
+ prefix: string | undefined,
178
+ relativePath: string,
179
+ ordered: boolean
180
+ ): MappedRoute => {
127
181
  const withoutExt = relativePath.slice(
128
182
  0,
129
183
  relativePath.length - extname(relativePath).length
130
184
  );
131
- const rawParts = withoutExt.split("/");
132
185
 
133
186
  const segments: string[] = [];
134
187
  const groups: string[] = [];
135
188
 
136
- for (const part of rawParts) {
137
- addRouteSegment(part, segments, groups);
189
+ for (const part of prefix ? prefix.split("/") : []) {
190
+ addRouteSegment(part, segments, groups, false);
191
+ }
192
+ for (const part of withoutExt.split("/")) {
193
+ addRouteSegment(part, segments, groups, ordered);
138
194
  }
139
195
 
140
196
  const route = segments.length === 0 ? "/" : `/${segments.join("/")}`;
@@ -156,7 +212,6 @@ const SETEXT_UNDERLINE = /^ {0,3}(?<marker>=+|-+)\s*$/u;
156
212
  const PARAGRAPH_INTERRUPT = /^ {0,3}(?:[-+*][ \t]|\d{1,9}[.)][ \t]|>)/u;
157
213
  const THEMATIC_BREAK =
158
214
  /^ {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$/u;
159
- const FRONT_MATTER_CLOSE = /^(?:-{3}|\.{3})\s*$/u;
160
215
  // `<Prompt>` renders its children into a permanently `hidden` DOM node (see
161
216
  // `Prompt.astro`) — the agent-facing prompt text is never visible page
162
217
  // content, only read by client JS for the copy button. Any `##` inside it
@@ -173,41 +228,13 @@ const PROMPT_OPEN = /^<Prompt(?![\w-])/u;
173
228
  // children text (`...copy this.</Prompt>`), not just sit on its own line.
174
229
  const PROMPT_CLOSE = /<\/Prompt>/u;
175
230
 
176
- /**
177
- * The body lines, minus a leading front matter block, plus the height of the
178
- * block that was dropped (`offset`) so line numbers can be reported against
179
- * the whole body. Bodies from the normalize pipeline are already
180
- * frontmatter-stripped, but `scanBody` also runs on raw documents — where a
181
- * leading `---` block (closed by `---` or `...`) is front matter, not a
182
- * thematic break whose closing `---` would underline the last metadata line
183
- * into a phantom setext heading.
184
- */
185
- const linesWithoutFrontMatter = (body: string) => {
186
- const lines = body.split("\n");
187
- if (!/^-{3}\s*$/u.test(lines[0] ?? "")) {
188
- return { lines, offset: 0 };
189
- }
190
- // A blank line directly after the dashes means the body *opens* with a
191
- // thematic break, not front matter — YAML metadata starts on the very next
192
- // line. Treating it as an unclosed block ate everything up to the next
193
- // `---`/`...` line of an already-stripped body.
194
- if ((lines[1] ?? "").trim() === "") {
195
- return { lines, offset: 0 };
196
- }
197
- const close = lines.findIndex(
198
- (line, index) => index > 0 && FRONT_MATTER_CLOSE.test(line)
199
- );
200
- return close === -1
201
- ? { lines, offset: 0 }
202
- : { lines: lines.slice(close + 1), offset: close + 1 };
203
- };
204
-
205
231
  // A trailing `{#id}` heading marker written without a backslash escape. Both
206
- // pipelines resolve escapes before parsing markers (see `ESCAPED_PUNCTUATION`),
207
- // so `\{#id\}` is the same marker — and the only spelling that survives the
208
- // MDX parser, where a bare `{…}` is a JSX expression and `#id` is not a valid
209
- // one (`Could not parse expression with acorn`). Further bracket markers may
210
- // follow the brace (`{#id} [toc]`), nothing else.
232
+ // pipelines resolve escapes before parsing markers (the scan parses each
233
+ // heading with the renderer's grammar — see `renderHeading`), so `\{#id\}` is
234
+ // the same marker — and the only spelling that survives the MDX parser, where
235
+ // a bare `{…}` is a JSX expression and `#id` is not a valid one (`Could not
236
+ // parse expression with acorn`). Further bracket markers may follow the brace
237
+ // (`{#id} [toc]`), nothing else.
211
238
  const BARE_CURLY_MARKER =
212
239
  /(?<!\\)\{#(?<id>[^\s}]+)\}(?:\s*\[(?:#[^\s\]]+|!?toc)\])*\s*$/u;
213
240
 
@@ -241,6 +268,8 @@ interface HeadingScanState {
241
268
  * end so a tag wrapped over several lines still matches.
242
269
  */
243
270
  anchorLines: string[];
271
+ /** The multi-line comment the scan is inside, if any — see `scanCommentLine`. */
272
+ comment: "html" | "jsx" | null;
244
273
  curlyMarkers: CurlyMarker[];
245
274
  fence: FenceState;
246
275
  /** 1-based body line of the line being scanned. */
@@ -299,12 +328,31 @@ const finishPromptTag = (
299
328
  * collapses `--`; github-slugger keeps it) and resolves repeated headings the
300
329
  * same way (`setup`, `setup-1`).
301
330
  */
302
- // CommonMark's escapable ASCII punctuation. The renderer only ever sees
303
- // heading text *after* the Markdown parser has resolved backslash escapes, so
304
- // `\[toc]` reaches the hast as plain `[toc]` and the marker still applies;
305
- // resolving escapes here keeps the two pipelines identical (there is no
306
- // inline way to write a literal trailing marker — use inline code instead).
307
- const ESCAPED_PUNCTUATION = /\\(?<char>[!-/:-@[-`{-~])/gu;
331
+ // A heading's inline Markdown is parsed with the renderer's own grammar:
332
+ // Blume's `.md` feature set plus the Astro defaults Blume never turns off, GFM
333
+ // and smart punctuation (Astro's `smartypants`). Front matter is off — the
334
+ // source is a lone heading. The display text is read without smart
335
+ // punctuation (see `toHeading`).
336
+ const HEADING_FEATURES = {
337
+ ...MARKDOWN_FEATURES,
338
+ frontmatter: false,
339
+ gfm: true,
340
+ };
341
+ const HEADING_PARSE = {
342
+ features: { ...HEADING_FEATURES, smartPunctuation: true },
343
+ };
344
+ const HEADING_TEXT_PARSE = {
345
+ features: { ...HEADING_FEATURES, smartPunctuation: false },
346
+ };
347
+
348
+ // Characters that can make a heading's rendered text differ from its source:
349
+ // escapes, code spans, emphasis/strikethrough/sub/superscript markers, link and
350
+ // image brackets, raw HTML and autolinks, entities, and the quotes, dashes, and
351
+ // ellipses smart punctuation rewrites. A heading with none of them renders as
352
+ // written, so it skips the parse.
353
+ const INLINE_MARKUP = /[\\`*_~^[\]<&'"]|--|\.\.\./u;
354
+ // The subset smart punctuation rewrites (`"a"` → `“a”`, `--` → `–`, `...` → `…`).
355
+ const SMART_PUNCTUATION = /['"]|--|\.\.\./u;
308
356
 
309
357
  // The start of a link-reference definition, as the renderer accepts it:
310
358
  // `[label]:` after up to 3 leading spaces, optionally inside block-quote or
@@ -322,10 +370,13 @@ const REF_DEFINITION =
322
370
 
323
371
  /**
324
372
  * The normalized labels of every link-reference definition in the body
325
- * (outside fenced code). A trailing heading bracket whose label is defined is
326
- * a CommonMark shortcut link, not a marker — the renderer leaves it in the
327
- * heading as an `<a>`, so the marker parse must skip it too. Labels match
328
- * case-insensitively with collapsed internal whitespace (CommonMark).
373
+ * (outside fenced code), footnote definitions (`^1`) included. A heading
374
+ * bracket whose label is defined is a CommonMark reference link —
375
+ * `[text][label]`, or a shortcut `[label]` that would otherwise read as a
376
+ * trailing `[toc]`/`[#id]` marker — or a GFM footnote reference, so each
377
+ * heading is parsed together with the definitions it names (see
378
+ * {@link definitionsFor}). Labels match case-insensitively with collapsed
379
+ * internal whitespace (CommonMark).
329
380
  *
330
381
  * Only a *valid* definition defines a label: the label must contain a
331
382
  * non-whitespace character, and a destination must follow — on the same line
@@ -357,14 +408,179 @@ const refDefinitionLabels = (lines: readonly string[]): Set<string> => {
357
408
  };
358
409
 
359
410
  /**
360
- * A heading record from raw heading text: escapes resolved and trailing
361
- * markers stripped, exactly as the renderer sees them. A `[#custom-id]` pin
362
- * becomes the slug verbatim and — matching the renderer — occupies its id in
363
- * the slugger, so a later heading whose auto-slug collides disambiguates
364
- * (`setup` → `setup-1`). `[!toc]`/`[toc]` headings stay in the record: their
365
- * ids exist in the rendered page, so links to them are valid anchors
366
- * regardless of TOC visibility. A heading that is nothing but markers keeps
367
- * them as literal text, mirroring the renderer.
411
+ * The link-reference and footnote definitions a heading's brackets may name,
412
+ * as source lines to parse the heading with. A definition the heading doesn't
413
+ * use changes nothing, so matching is loose (the label anywhere in the folded
414
+ * text). A footnote label (`^1`) is defined too, so `[^1]` parses as the
415
+ * footnote reference it renders as, not as literal text.
416
+ */
417
+ const definitionsFor = (raw: string, labels: ReadonlySet<string>): string => {
418
+ if (!raw.includes("[")) {
419
+ return "";
420
+ }
421
+ const folded = raw.replaceAll(/\s+/gu, " ").toLowerCase();
422
+ let definitions = "";
423
+ for (const label of labels) {
424
+ if (folded.includes(label)) {
425
+ definitions += `\n\n[${label}]: /`;
426
+ }
427
+ }
428
+ return definitions;
429
+ };
430
+
431
+ /**
432
+ * The number each footnote renders as, keyed by identifier. GFM numbers
433
+ * footnotes in the order the body first references them, whatever order their
434
+ * definitions sit in, so a `[^b]` cited before `[^a]` renders as 1. A
435
+ * reference inside a footnote definition is left out: the renderer reaches
436
+ * those only after the body.
437
+ */
438
+ const footnoteNumbers = (body: string): Map<string, number> => {
439
+ const numbers = new Map<string, number>();
440
+ const visit = (nodes: readonly Nodes[]): void => {
441
+ for (const node of nodes) {
442
+ if (node.type === "footnoteReference") {
443
+ if (!numbers.has(node.identifier)) {
444
+ numbers.set(node.identifier, numbers.size + 1);
445
+ }
446
+ } else if (node.type !== "footnoteDefinition" && "children" in node) {
447
+ visit(node.children);
448
+ }
449
+ }
450
+ };
451
+ const tree = markdownToMdast(body, HEADING_PARSE);
452
+ visit("children" in tree ? tree.children : []);
453
+ return numbers;
454
+ };
455
+
456
+ /** What reading one heading needs from the rest of its body. */
457
+ interface HeadingContext {
458
+ /**
459
+ * The number each footnote renders as (see {@link footnoteNumbers}),
460
+ * computed on first use: only a heading that cites a footnote needs it.
461
+ */
462
+ footnotes: () => ReadonlyMap<string, number>;
463
+ /** The body's link-reference and footnote definition labels. */
464
+ labels: ReadonlySet<string>;
465
+ }
466
+
467
+ /**
468
+ * The text content of mdast inline nodes, the way the rendered heading reads
469
+ * it: link and emphasis text, code-span contents, decoded entities, and a
470
+ * footnote reference's number. Raw HTML tags add no text (the text between
471
+ * them is its own node), and an image's alt is an attribute rather than text
472
+ * content.
473
+ */
474
+ const inlineText = (nodes: readonly Nodes[], context: HeadingContext): string =>
475
+ nodes
476
+ .map((node) => {
477
+ if (
478
+ node.type === "html" ||
479
+ node.type === "image" ||
480
+ node.type === "imageReference"
481
+ ) {
482
+ return "";
483
+ }
484
+ if (node.type === "break") {
485
+ return "\n";
486
+ }
487
+ if (node.type === "footnoteReference") {
488
+ // A reference the body's own parse never numbered (a heading the scan
489
+ // finds inside an HTML block) is no footnote to the renderer either.
490
+ const number = context.footnotes().get(node.identifier);
491
+ return number === undefined
492
+ ? `[^${node.label ?? node.identifier}]`
493
+ : String(number);
494
+ }
495
+ if ("children" in node) {
496
+ return inlineText(node.children, context);
497
+ }
498
+ return "value" in node ? node.value : "";
499
+ })
500
+ .join("");
501
+
502
+ /** A heading's rendered text plus the trailing markers the renderer strips. */
503
+ interface RenderedHeading {
504
+ /** Author-pinned anchor id from `[#id]`/`{#id}`, used verbatim. */
505
+ id?: string;
506
+ /** The text content the renderer slugs, markers stripped, untrimmed. */
507
+ text: string;
508
+ toc?: "hide" | "only";
509
+ }
510
+
511
+ /** A heading whose source is its text: one text node, markers stripped. */
512
+ const plainHeading = (raw: string): RenderedHeading => {
513
+ const markers = parseHeadingMarkers(raw);
514
+ // A heading that is nothing but markers keeps them as literal text.
515
+ return markers.text === ""
516
+ ? { text: raw }
517
+ : { id: markers.id, text: markers.text, toc: markers.toc };
518
+ };
519
+
520
+ /**
521
+ * Parse a heading's inline Markdown and read it the way
522
+ * `markdown/heading-anchors` does: the heading's text content, with trailing
523
+ * markers stripped from its last node only when that node is text (a heading
524
+ * ending in inline code or a link has no marker position) and some heading
525
+ * text remains. Parsing (rather than regex-stripping) resolves escapes —
526
+ * `\[toc]` still reaches the marker parse as `[toc]` — and emphasis the way
527
+ * CommonMark does, so `snake_case` stays whole while `_note_` loses its
528
+ * underscores.
529
+ */
530
+ const renderHeading = (
531
+ raw: string,
532
+ form: "atx" | "setext",
533
+ context: HeadingContext,
534
+ parse: typeof HEADING_PARSE
535
+ ): RenderedHeading => {
536
+ if (!INLINE_MARKUP.test(raw)) {
537
+ return plainHeading(raw);
538
+ }
539
+ const source = form === "atx" ? `# ${raw}` : `${raw}\n=`;
540
+ const tree = markdownToMdast(
541
+ `${source}${definitionsFor(raw, context.labels)}`,
542
+ parse
543
+ );
544
+ const heading = "children" in tree ? tree.children.at(0) : undefined;
545
+ // The scan's paragraph tracking is coarser than the parser's: underlined
546
+ // text that opens with an HTML block or a definition is no heading to the
547
+ // renderer. It is read as written.
548
+ if (heading?.type !== "heading") {
549
+ return plainHeading(raw);
550
+ }
551
+ const { children } = heading;
552
+ const text = inlineText(children, context);
553
+ const last = children.at(-1);
554
+ if (last?.type !== "text") {
555
+ return { text };
556
+ }
557
+ const markers = parseHeadingMarkers(last.value);
558
+ const stripped = last.value.length - markers.text.length;
559
+ if (stripped === 0 || (markers.text === "" && children.length === 1)) {
560
+ return { text };
561
+ }
562
+ return {
563
+ id: markers.id,
564
+ text: text.slice(0, text.length - stripped),
565
+ toc: markers.toc,
566
+ };
567
+ };
568
+
569
+ /**
570
+ * A heading record from raw heading text, read exactly as the renderer reads
571
+ * it: inline Markdown reduced to its rendered text (see {@link renderHeading})
572
+ * and trailing markers stripped. The slug is the renderer's slug of that text.
573
+ * A `[#custom-id]` pin becomes the slug verbatim and — matching the renderer —
574
+ * occupies its id in the slugger, so a later heading whose auto-slug collides
575
+ * disambiguates (`setup` → `setup-1`). `[!toc]`/`[toc]` headings stay in the
576
+ * record: their ids exist in the rendered page, so links to them are valid
577
+ * anchors regardless of TOC visibility.
578
+ *
579
+ * The record's `text` — a page's fallback title and sidebar label — is the
580
+ * same rendered text with whitespace collapsed, but read without smart
581
+ * punctuation: it keeps the straight quotes and double hyphens the author
582
+ * typed, the way a frontmatter `title` does, and the way an Obsidian
583
+ * `[[Note#It's here]]` heading link spells the heading it resolves against.
368
584
  */
369
585
  /** A scanned heading plus whether its id came from an author pin. */
370
586
  interface ScannedHeading {
@@ -375,23 +591,23 @@ interface ScannedHeading {
375
591
  const toHeading = (
376
592
  depth: number,
377
593
  raw: string,
594
+ form: "atx" | "setext",
378
595
  slugger: GithubSlugger,
379
- isRefDefined: (label: string) => boolean
596
+ context: HeadingContext
380
597
  ): ScannedHeading => {
381
- const unescaped = raw.replaceAll(ESCAPED_PUNCTUATION, "$<char>");
382
- const markers = parseHeadingMarkers(unescaped, isRefDefined);
383
- const text = markers.text.trim();
384
- if (text === "" && (markers.id !== undefined || markers.toc !== undefined)) {
385
- return {
386
- heading: { depth, slug: slugger.slug(unescaped), text: unescaped },
387
- pinned: false,
388
- };
389
- }
390
- if (markers.id !== undefined) {
391
- occupySlug(slugger, markers.id);
392
- return { heading: { depth, slug: markers.id, text }, pinned: true };
598
+ const rendered = renderHeading(raw, form, context, HEADING_PARSE);
599
+ const display = SMART_PUNCTUATION.test(raw)
600
+ ? renderHeading(raw, form, context, HEADING_TEXT_PARSE).text
601
+ : rendered.text;
602
+ const text = display.replaceAll(/[\t\n\f\r ]+/gu, " ").trim();
603
+ if (rendered.id !== undefined) {
604
+ occupySlug(slugger, rendered.id);
605
+ return { heading: { depth, slug: rendered.id, text }, pinned: true };
393
606
  }
394
- return { heading: { depth, slug: slugger.slug(text), text }, pinned: false };
607
+ return {
608
+ heading: { depth, slug: slugger.slug(rendered.text), text },
609
+ pinned: false,
610
+ };
395
611
  };
396
612
 
397
613
  /** Record a heading and where a trailing marker would be written for it. */
@@ -423,7 +639,7 @@ const scanContentLine = (
423
639
  state: HeadingScanState,
424
640
  slugger: GithubSlugger,
425
641
  headings: Heading[],
426
- isRefDefined: (label: string) => boolean
642
+ context: HeadingContext
427
643
  ): void => {
428
644
  // Inline code is masked so a documented `<a id="…">` isn't an anchor.
429
645
  state.anchorLines.push(line.replaceAll(INLINE_CODE, ""));
@@ -434,7 +650,7 @@ const scanContentLine = (
434
650
  pushHeading(
435
651
  headings,
436
652
  state,
437
- toHeading(depth, text, slugger, isRefDefined),
653
+ toHeading(depth, text, "atx", slugger, context),
438
654
  state.line
439
655
  );
440
656
  noteCurlyMarker(text, state.line, state);
@@ -444,15 +660,17 @@ const scanContentLine = (
444
660
  const setext = line.match(SETEXT_UNDERLINE);
445
661
  if (setext?.groups && state.paragraph.length > 0) {
446
662
  // Setext wins over thematic break when it closes a paragraph (CommonMark);
447
- // a multi-line paragraph renders as one heading, soft breaks as spaces.
663
+ // a multi-line paragraph renders as one heading. Its lines keep their line
664
+ // breaks, which the rendered text content keeps too (and the slugger
665
+ // drops, so `Multi\nline` anchors as `multiline`).
448
666
  const depth = setext.groups.marker?.startsWith("=") ? 1 : 2;
449
- const text = state.paragraph.join(" ").trim();
667
+ const text = state.paragraph.join("\n");
450
668
  // A setext heading's markers trail its last text line, just above the
451
669
  // underline — that is where a pin is appended.
452
670
  pushHeading(
453
671
  headings,
454
672
  state,
455
- toHeading(depth, text, slugger, isRefDefined),
673
+ toHeading(depth, text, "setext", slugger, context),
456
674
  state.line - 1
457
675
  );
458
676
  noteCurlyMarker(text, state.paragraphStart, state);
@@ -473,14 +691,68 @@ const scanContentLine = (
473
691
  state.paragraph.push(line.trim());
474
692
  };
475
693
 
694
+ // A comment that opens a line hides everything up to its close from the
695
+ // reader: an HTML comment (`<!--` … `-->`, a CommonMark HTML block, so up to
696
+ // 3 leading spaces) or an MDX JSX one (`{/*` … `*/}`, any indentation — MDX
697
+ // has no indented code).
698
+ const HTML_COMMENT_OPEN = /^ {0,3}<!--/u;
699
+ const JSX_COMMENT_OPEN = /^\s*\{\/\*/u;
700
+ const JSX_COMMENT_CLOSE = /\*\/\s*\}/u;
701
+
702
+ /**
703
+ * Advance the multi-line comment state over one line, returning whether the
704
+ * line belongs to a comment. A commented-out `# Old title` renders nothing, so
705
+ * it must not become a heading — the page title, a TOC entry, or an anchor.
706
+ * A comment that closes on its opening line is left to the regular scan,
707
+ * which never reads a line starting with `<` or `{` as a heading. The
708
+ * CommonMark end condition is any `-->` on the line, the opening one
709
+ * included.
710
+ */
711
+ const scanCommentLine = (line: string, state: HeadingScanState): boolean => {
712
+ if (state.comment === "html") {
713
+ if (line.includes("-->")) {
714
+ state.comment = null;
715
+ }
716
+ return true;
717
+ }
718
+ if (state.comment === "jsx") {
719
+ if (JSX_COMMENT_CLOSE.test(line)) {
720
+ state.comment = null;
721
+ }
722
+ return true;
723
+ }
724
+ if (HTML_COMMENT_OPEN.test(line) && !line.includes("-->")) {
725
+ state.comment = "html";
726
+ return true;
727
+ }
728
+ const jsx = JSX_COMMENT_OPEN.exec(line);
729
+ if (jsx && !JSX_COMMENT_CLOSE.test(line.slice(jsx[0].length))) {
730
+ state.comment = "jsx";
731
+ return true;
732
+ }
733
+ return false;
734
+ };
735
+
476
736
  /** Scan one line for a heading, advancing the fence/paragraph state. */
477
737
  const scanHeadingLine = (
478
738
  line: string,
479
739
  state: HeadingScanState,
480
740
  slugger: GithubSlugger,
481
741
  headings: Heading[],
482
- isRefDefined: (label: string) => boolean
742
+ context: HeadingContext
483
743
  ): void => {
744
+ // Comments hide fences too, and fences and prompts hide comments. A comment
745
+ // line still goes to the anchor pass, which strips HTML comments itself.
746
+ if (
747
+ state.fence === null &&
748
+ state.promptDepth === 0 &&
749
+ !state.promptTag &&
750
+ scanCommentLine(line, state)
751
+ ) {
752
+ state.anchorLines.push(line.replaceAll(INLINE_CODE, ""));
753
+ state.paragraph = [];
754
+ return;
755
+ }
484
756
  const next = nextFenceState(line, state.fence);
485
757
  // Skip fence delimiter lines themselves and anything inside a fence. A fence
486
758
  // also ends any open paragraph, so no underline can reach across it.
@@ -511,7 +783,7 @@ const scanHeadingLine = (
511
783
  state.paragraph = [];
512
784
  return;
513
785
  }
514
- scanContentLine(line, state, slugger, headings, isRefDefined);
786
+ scanContentLine(line, state, slugger, headings, context);
515
787
  };
516
788
 
517
789
  /** Where a heading's text ends in the scanned text, for appending a marker. */
@@ -541,13 +813,17 @@ export interface BodyScan {
541
813
  /**
542
814
  * Scan a body for its headings, explicit HTML anchors, and unescaped `{#id}`
543
815
  * markers in one fence-aware walk (the same walk `extractHeadings` exposes for
544
- * headings alone).
816
+ * headings alone). The body is the page's content with its front matter
817
+ * already stripped — the renderers read front matter off the page and never
818
+ * again from its body — so a leading `---` block is a thematic break and
819
+ * content, in `.md` and `.mdx` alike.
545
820
  */
546
821
  export const scanBody = (body: string): BodyScan => {
547
822
  const headings: Heading[] = [];
548
823
  const slugger = new GithubSlugger();
549
824
  const state: HeadingScanState = {
550
825
  anchorLines: [],
826
+ comment: null,
551
827
  curlyMarkers: [],
552
828
  fence: null,
553
829
  line: 0,
@@ -558,13 +834,18 @@ export const scanBody = (body: string): BodyScan => {
558
834
  sites: [],
559
835
  };
560
836
 
561
- const { lines, offset } = linesWithoutFrontMatter(body);
562
- const definedLabels = refDefinitionLabels(lines);
563
- const isRefDefined = (label: string): boolean =>
564
- definedLabels.has(label.toLowerCase());
837
+ const lines = body.split("\n");
838
+ let footnotes: Map<string, number> | undefined;
839
+ const context: HeadingContext = {
840
+ footnotes: () => {
841
+ footnotes ??= footnoteNumbers(lines.join("\n"));
842
+ return footnotes;
843
+ },
844
+ labels: refDefinitionLabels(lines),
845
+ };
565
846
  for (const [index, line] of lines.entries()) {
566
- state.line = index + offset + 1;
567
- scanHeadingLine(line, state, slugger, headings, isRefDefined);
847
+ state.line = index + 1;
848
+ scanHeadingLine(line, state, slugger, headings, context);
568
849
  }
569
850
 
570
851
  const anchors = new Set<string>();
@@ -792,6 +1073,14 @@ export const strippedLineOffset = (
792
1073
  ): number =>
793
1074
  raw ? Math.max(0, raw.split("\n").length - body.split("\n").length) : 0;
794
1075
 
1076
+ /**
1077
+ * How many lines of the entry's source file sit above its body: what the
1078
+ * source reported (`bodyLineOffset`, when `raw` is a rewrite of the file), or
1079
+ * else the height of `raw`'s stripped front matter.
1080
+ */
1081
+ const entryLineOffset = (entry: SourceEntry): number =>
1082
+ entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
1083
+
795
1084
  /**
796
1085
  * Map links extracted from include-expanded text back to the file and raw
797
1086
  * line each expanded line came from, so a broken link inside a partial is
@@ -834,10 +1123,7 @@ const entryLinks = (entry: SourceEntry): PageLink[] =>
834
1123
  entry.expanded.origins,
835
1124
  entry.sourcePath
836
1125
  )
837
- : extractLinks(
838
- entry.body.text,
839
- strippedLineOffset(entry.raw, entry.body.text)
840
- );
1126
+ : extractLinks(entry.body.text, entryLineOffset(entry));
841
1127
 
842
1128
  /**
843
1129
  * Diagnostics for `{#id}` heading markers in an `.mdx` page. The MDX parser
@@ -860,8 +1146,7 @@ const curlyMarkerDiagnostics = (
860
1146
  return {
861
1147
  code: "BLUME_MDX_CURLY_ANCHOR",
862
1148
  file: origin?.file ?? page,
863
- line:
864
- origin?.line ?? line + strippedLineOffset(entry.raw, entry.body.text),
1149
+ line: origin?.line ?? line + entryLineOffset(entry),
865
1150
  message: inPartial
866
1151
  ? `\`{#${id}}\` is a JSX expression once this partial is included in ${page} (.mdx), so that page fails to compile.`
867
1152
  : `\`{#${id}}\` is a JSX expression in .mdx, so this page fails to compile.`,
@@ -882,8 +1167,17 @@ const deriveTitle = (
882
1167
  if (firstHeading) {
883
1168
  return firstHeading.text;
884
1169
  }
885
- const base = id.split("/").pop() ?? id;
886
- return titleCase(stripNumericPrefix(base.replace(extname(base), "")));
1170
+ const parts = id.split("/");
1171
+ const base = parts.at(-1) ?? id;
1172
+ const stem = stripNumericPrefix(base.replace(extname(base), ""));
1173
+ // An index page stands for its folder, so an untitled one takes the label
1174
+ // the sidebar gives that folder (`guides/index.md` is "Guides"). The
1175
+ // content root's own index has no folder to borrow from.
1176
+ const folder = stem === "index" ? parts.at(-2) : undefined;
1177
+ if (folder !== undefined) {
1178
+ return titleCase(stripNumericPrefix(orderedGroupLabel(folder) ?? folder));
1179
+ }
1180
+ return titleCase(stem);
887
1181
  };
888
1182
 
889
1183
  /** Strip habitual leading/trailing slashes (`/getting-started`, `guides/`). */
@@ -902,6 +1196,13 @@ const withPrefix = (prefix: string | undefined, path: string): string => {
902
1196
 
903
1197
  /** What a route resolution needs from the owning source and the config. */
904
1198
  export type RouteContext = Pick<NormalizeContext, "i18n" | "versions"> & {
1199
+ /**
1200
+ * Whether the entry's ref is a path of file and folder names whose ordering
1201
+ * prefixes (`01-intro`) sort the sidebar and drop from the route: true for
1202
+ * filesystem sources. A staged source's ref is a slug, a release tag, or a
1203
+ * note name, and keeps its leading numbers.
1204
+ */
1205
+ orderingPrefixes?: boolean;
905
1206
  /** The source's route prefix (`NormalizeContext["source"]["prefix"]`). */
906
1207
  prefix?: string;
907
1208
  };
@@ -970,7 +1271,9 @@ export interface EntryRoute extends Pick<
970
1271
  * A frontmatter `slug` wins, then the adapter-supplied `entry.slug` (the typed
971
1272
  * SPI's "logical route input; defaults to ref if omitted"), then the ref. The
972
1273
  * extension is re-appended so `mapRoute`'s extname strip can't eat a dotted
973
- * slug segment (`v1.2`). A slug that trims to nothing falls back. The version
1274
+ * slug segment (`v1.2`). A slug that trims to nothing falls back. Only a
1275
+ * filesystem ref (`ctx.orderingPrefixes`) loses its ordering prefixes; a slug
1276
+ * is a route spelled out, so `2024-year-in-review` stays whole. The version
974
1277
  * prefixes the mapped route *after* `mapRoute` runs: the mapped route is the
975
1278
  * version-agnostic key, the config id is prepended verbatim (never
976
1279
  * numeric-prefix-stripped), a frontmatter `slug` gets versionized so snapshots
@@ -987,8 +1290,18 @@ export const resolveEntryRoute = (
987
1290
  const { locales, navPath, version } = placeEntryRef(entry.ref, ext, ctx);
988
1291
  const slugInput = frontmatterSlug ?? entry.slug;
989
1292
  const slug = slugInput ? trimSlashes(slugInput) : "";
990
- const routeInput = withPrefix(ctx.prefix, slug ? `${slug}${ext}` : navPath);
991
- const { segments, groups, route: versionKey } = mapRoute(routeInput);
1293
+ // A frontmatter slug is a route spelled out; an adapter's `entry.slug` is
1294
+ // one too unless the source's names are ordered file names (a vault note's
1295
+ // path), which lose their prefixes like the ref does.
1296
+ const ordered =
1297
+ ctx.orderingPrefixes === true && frontmatterSlug === undefined;
1298
+ const {
1299
+ segments,
1300
+ groups,
1301
+ route: versionKey,
1302
+ } = slug
1303
+ ? mapRoute(ctx.prefix, `${slug}${ext}`, ordered)
1304
+ : mapRoute(ctx.prefix, navPath, ctx.orderingPrefixes === true);
992
1305
  return {
993
1306
  groups,
994
1307
  locales,
@@ -1081,6 +1394,25 @@ const validateCustomKeys = (
1081
1394
  };
1082
1395
  };
1083
1396
 
1397
+ // A `.` or `..` path segment, which a browser resolves away before requesting.
1398
+ const DOT_SEGMENT = /(?:^|\/)\.{1,2}(?:\/|$)/u;
1399
+
1400
+ /**
1401
+ * A frontmatter `slug` with a `.` or `..` segment names no reachable URL: a
1402
+ * browser normalizes the link (`guides/./x` → `guides/x`, `../x` → `/x`)
1403
+ * before requesting it, and `..` would have Astro write the page outside the
1404
+ * build output. Rejected alongside the schema's own errors.
1405
+ */
1406
+ const slugIssues = (slug: string | undefined): CustomKeyIssue[] =>
1407
+ slug !== undefined && DOT_SEGMENT.test(slug)
1408
+ ? [
1409
+ {
1410
+ message: `"${slug}" has a "." or ".." segment, which browsers resolve away, so no link could reach the page. Write the route out from the content root.`,
1411
+ path: ["slug"],
1412
+ },
1413
+ ]
1414
+ : [];
1415
+
1084
1416
  /**
1085
1417
  * Parse an entry's frontmatter: built-in keys through the strict page schema,
1086
1418
  * custom keys (`frontmatter.extend` plus the page type's
@@ -1119,8 +1451,12 @@ const parseEntryMeta = (
1119
1451
 
1120
1452
  const result = pageMetaSchema.safeParse(known);
1121
1453
  const customResult = extend ? validateCustomKeys(entry.data, extend) : null;
1454
+ const issues = [
1455
+ ...(result.success ? slugIssues(result.data.slug) : []),
1456
+ ...(customResult?.issues ?? []),
1457
+ ];
1122
1458
 
1123
- if (result.success && (customResult?.issues.length ?? 0) === 0) {
1459
+ if (result.success && issues.length === 0) {
1124
1460
  return { custom: customResult?.custom, meta: result.data };
1125
1461
  }
1126
1462
 
@@ -1140,9 +1476,7 @@ const parseEntryMeta = (
1140
1476
  return {
1141
1477
  diagnostics: [
1142
1478
  ...(result.success ? [] : diagnosticsFromZod(result.error, location)),
1143
- ...(customResult
1144
- ? diagnosticsFromIssues(customResult.issues, location)
1145
- : []),
1479
+ ...diagnosticsFromIssues(issues, location),
1146
1480
  ],
1147
1481
  };
1148
1482
  };
@@ -1192,6 +1526,7 @@ export const normalizeEntry = (
1192
1526
  versionKey,
1193
1527
  } = resolveEntryRoute(entry, ext, meta.slug, {
1194
1528
  i18n: ctx.i18n,
1529
+ orderingPrefixes: !ctx.source.staged || ctx.source.orderedNames === true,
1195
1530
  prefix: ctx.source.prefix,
1196
1531
  versions: ctx.versions,
1197
1532
  });
@@ -1237,10 +1572,12 @@ export const normalizeEntry = (
1237
1572
  // `basePath` is applied outermost — after locale prefixing — so the route
1238
1573
  // reads `{basePath}/{locale?}/{prefix?}/…`; `navPath` and `translationKey`
1239
1574
  // stay base-less so the nav tree and translation matching are unaffected.
1575
+ // The base is mounted unconditionally: a `docs/` folder under a `/docs`
1576
+ // base is a real `/docs/docs/…` route, not an already-based one.
1240
1577
  const pages = locales.map((locale) => ({
1241
1578
  ...base,
1242
1579
  locale,
1243
- route: withBasePath(
1580
+ route: mountBasePath(
1244
1581
  ctx.basePath ?? "",
1245
1582
  localizedRoute(logicalRoute, locale, ctx.i18n)
1246
1583
  ),