blume 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (601) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +113 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-6k4ftwze.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-6k4ftwze.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-zp79m0ts.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-ddndchfr.js → chunk-2f3ndkpw.js} +57 -29
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-fsmrqk8a.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-ppzjqwx2.js → chunk-2n4ps1b3.js} +17 -18
  12. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-2n4ps1b3.js.map} +3 -3
  13. package/dist/cli/{chunk-5r8g91qn.js → chunk-43zcf4gz.js} +398 -159
  14. package/dist/cli/chunk-43zcf4gz.js.map +36 -0
  15. package/dist/cli/chunk-4ejxbzee.js +39 -0
  16. package/dist/cli/chunk-4ejxbzee.js.map +10 -0
  17. package/dist/cli/{chunk-hr8ne106.js → chunk-4h8z9vv5.js} +296 -77
  18. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  19. package/dist/cli/{chunk-k7pj68a8.js → chunk-5cg2rzgf.js} +18 -16
  20. package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5cg2rzgf.js.map} +2 -2
  21. package/dist/cli/{chunk-h2ez8dzb.js → chunk-5ffywsmb.js} +4 -4
  22. package/dist/cli/{chunk-g698a744.js → chunk-5wj2qwn2.js} +5 -5
  23. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  24. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  25. package/dist/cli/{chunk-3yce002v.js → chunk-713mngdq.js} +15 -8
  26. package/dist/cli/chunk-713mngdq.js.map +14 -0
  27. package/dist/cli/{chunk-91ws1n6j.js → chunk-9geymqns.js} +17 -15
  28. package/dist/cli/{chunk-91ws1n6j.js.map → chunk-9geymqns.js.map} +2 -2
  29. package/dist/cli/{chunk-6dtt0zfn.js → chunk-9xr4dgee.js} +27 -18
  30. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  31. package/dist/cli/{chunk-sqw4ekg1.js → chunk-amrb6hgh.js} +2 -2
  32. package/dist/cli/chunk-cm89scdv.js +443 -0
  33. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  34. package/dist/cli/{chunk-wdrt2k2v.js → chunk-cmtcnh9f.js} +90 -26
  35. package/dist/cli/chunk-cmtcnh9f.js.map +10 -0
  36. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  37. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  38. package/dist/cli/{chunk-7mbqtmgb.js → chunk-e7vvk5ya.js} +21 -12
  39. package/dist/cli/chunk-e7vvk5ya.js.map +10 -0
  40. package/dist/cli/{chunk-qkb5a8sa.js → chunk-g2z9x01v.js} +6 -98
  41. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  42. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  43. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  44. package/dist/cli/{chunk-h7k3nq3v.js → chunk-gp84za0h.js} +2 -2
  45. package/dist/cli/{chunk-hqp2ajnh.js → chunk-h2a48yhd.js} +195 -73
  46. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  47. package/dist/cli/{chunk-pbg5a4s3.js → chunk-jnwj5kjj.js} +71 -47
  48. package/dist/cli/chunk-jnwj5kjj.js.map +19 -0
  49. package/dist/cli/chunk-kwrg63tm.js +31 -0
  50. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  51. package/dist/cli/{chunk-8g8ytmgx.js → chunk-nd1821sk.js} +21 -19
  52. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-nd1821sk.js.map} +2 -2
  53. package/dist/cli/{chunk-w4bxdvsa.js → chunk-ntv4qrt7.js} +17 -15
  54. package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-ntv4qrt7.js.map} +2 -2
  55. package/dist/cli/{chunk-bfwp9vp6.js → chunk-p804f3pe.js} +7 -7
  56. package/dist/cli/{chunk-4e9b9ra6.js → chunk-py62fewh.js} +6 -6
  57. package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-py62fewh.js.map} +1 -1
  58. package/dist/cli/chunk-q3wxxq39.js +126 -0
  59. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  60. package/dist/cli/{chunk-j85scx15.js → chunk-rekqz4qg.js} +142 -46
  61. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  62. package/dist/cli/{chunk-00gs3wqs.js → chunk-ta27feev.js} +1 -1
  63. package/dist/cli/{chunk-v6ya5kcb.js → chunk-xkz5xbtv.js} +4031 -1132
  64. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  65. package/dist/cli/{chunk-bbnwccaz.js → chunk-y4ykbark.js} +2 -2
  66. package/dist/cli/{chunk-n1yg3tj3.js → chunk-y5h4r2y0.js} +6 -6
  67. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-y5h4r2y0.js.map} +3 -3
  68. package/dist/cli/{chunk-nfcyttvj.js → chunk-yj17vs39.js} +9 -9
  69. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-yj17vs39.js.map} +1 -1
  70. package/dist/cli/{chunk-7vtckvaw.js → chunk-yxmk4pec.js} +16 -14
  71. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-yxmk4pec.js.map} +2 -2
  72. package/dist/cli/{chunk-gs7r695n.js → chunk-zccbjcj0.js} +22 -8
  73. package/dist/cli/{chunk-gs7r695n.js.map → chunk-zccbjcj0.js.map} +3 -3
  74. package/dist/cli/{chunk-xh43dwgw.js → chunk-zf748avv.js} +415 -225
  75. package/dist/cli/chunk-zf748avv.js.map +37 -0
  76. package/dist/cli/{chunk-d1v5rhy0.js → chunk-zhvd6adm.js} +16 -14
  77. package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-zhvd6adm.js.map} +2 -2
  78. package/dist/cli/index.js +28 -20
  79. package/dist/cli/index.js.map +5 -5
  80. package/dist/types/ai/agent-surface.d.ts +8 -7
  81. package/dist/types/ai/ai-catalog.d.ts +7 -0
  82. package/dist/types/ai/ask-context.d.ts +22 -0
  83. package/dist/types/ai/ask.d.ts +271 -58
  84. package/dist/types/ai/index.d.ts +2 -2
  85. package/dist/types/ai/link-headers.d.ts +8 -1
  86. package/dist/types/ai/openapi-components.d.ts +5 -2
  87. package/dist/types/ai/relative-links.d.ts +9 -7
  88. package/dist/types/ai/site-skill.d.ts +26 -0
  89. package/dist/types/ai/skills.d.ts +20 -1
  90. package/dist/types/ai/tar.d.ts +1 -3
  91. package/dist/types/analytics/index.d.ts +2 -0
  92. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  93. package/dist/types/analytics/schema.d.ts +14 -0
  94. package/dist/types/astro/integration.d.ts +11 -2
  95. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  96. package/dist/types/captcha/index.d.ts +28 -0
  97. package/dist/types/captcha/schema.d.ts +29 -0
  98. package/dist/types/captcha/turnstile.d.ts +28 -0
  99. package/dist/types/components/content/api-field.d.ts +59 -0
  100. package/dist/types/components/content/api-page.d.ts +95 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/helpers.d.ts +137 -0
  103. package/dist/types/components/openapi/security.d.ts +85 -0
  104. package/dist/types/consent/ethyca.d.ts +48 -0
  105. package/dist/types/consent/index.d.ts +37 -0
  106. package/dist/types/consent/native.d.ts +28 -0
  107. package/dist/types/consent/osano.d.ts +42 -0
  108. package/dist/types/consent/schema.d.ts +110 -0
  109. package/dist/types/core/base-path.d.ts +21 -7
  110. package/dist/types/core/config-input.d.ts +196 -27
  111. package/dist/types/core/data.d.ts +49 -4
  112. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  113. package/dist/types/core/footer.d.ts +8 -0
  114. package/dist/types/core/heading-markers.d.ts +5 -7
  115. package/dist/types/core/i18n-ui.d.ts +86 -0
  116. package/dist/types/core/i18n.d.ts +2 -0
  117. package/dist/types/core/includes.d.ts +8 -2
  118. package/dist/types/core/last-modified.d.ts +10 -0
  119. package/dist/types/core/localizable.d.ts +7 -0
  120. package/dist/types/core/meta.d.ts +8 -0
  121. package/dist/types/core/page-modes.d.ts +29 -0
  122. package/dist/types/core/redirect-patterns.d.ts +144 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +558 -56
  125. package/dist/types/core/sources/lower.d.ts +29 -16
  126. package/dist/types/core/sources/normalize.d.ts +13 -1
  127. package/dist/types/core/sources/read.d.ts +5 -0
  128. package/dist/types/core/sources/watch.d.ts +12 -5
  129. package/dist/types/core/standard-schema.d.ts +5 -0
  130. package/dist/types/core/types.d.ts +1 -1
  131. package/dist/types/core/variables.d.ts +42 -0
  132. package/dist/types/deploy/artifacts.d.ts +6 -4
  133. package/dist/types/deploy/headers.d.ts +5 -0
  134. package/dist/types/deploy/node-headers.d.ts +13 -2
  135. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  136. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  137. package/dist/types/deploy/platforms/types.d.ts +8 -0
  138. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  139. package/dist/types/deploy/redirects.d.ts +61 -22
  140. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  141. package/dist/types/markdown/code-title.d.ts +17 -0
  142. package/dist/types/markdown/directives.d.ts +62 -0
  143. package/dist/types/markdown/features.d.ts +24 -2
  144. package/dist/types/markdown/mdast.d.ts +63 -0
  145. package/dist/types/narration/build.d.ts +70 -0
  146. package/dist/types/narration/provider.d.ts +26 -0
  147. package/dist/types/narration/script.d.ts +154 -0
  148. package/dist/types/openapi/asyncapi.d.ts +4 -2
  149. package/dist/types/openapi/model.d.ts +12 -6
  150. package/dist/types/openapi/overlay.d.ts +54 -0
  151. package/dist/types/openapi/parse.d.ts +13 -0
  152. package/dist/types/openapi/references.d.ts +14 -4
  153. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  154. package/dist/types/ratelimit/index.d.ts +27 -0
  155. package/dist/types/ratelimit/memory.d.ts +36 -0
  156. package/dist/types/ratelimit/schema.d.ts +111 -0
  157. package/dist/types/ratelimit/upstash.d.ts +21 -0
  158. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  159. package/dist/types/reference/asyncapi.d.ts +10 -10
  160. package/dist/types/reference/graphql.d.ts +13 -10
  161. package/dist/types/reference/openapi.d.ts +33 -12
  162. package/dist/types/reference/options.d.ts +45 -5
  163. package/dist/types/reference/scalar.d.ts +13 -0
  164. package/dist/types/reference/schema.d.ts +60 -36
  165. package/dist/types/search/documents.d.ts +12 -1
  166. package/dist/types/search/orama-index.d.ts +4 -0
  167. package/dist/types/seo/metatags.d.ts +20 -0
  168. package/docs/02-deployment.mdx +44 -6
  169. package/docs/03-upgrading.mdx +4 -4
  170. package/docs/04-migrating.mdx +1 -1
  171. package/docs/08-faq.mdx +1 -1
  172. package/docs/advanced/changelog.mdx +2 -2
  173. package/docs/advanced/custom-pages.mdx +3 -3
  174. package/docs/advanced/skills.mdx +14 -0
  175. package/docs/cli/audit.mdx +38 -2
  176. package/docs/cli/evals.mdx +3 -3
  177. package/docs/cli/index.mdx +1 -0
  178. package/docs/cli/translate.mdx +3 -3
  179. package/docs/cli/version.mdx +1 -1
  180. package/docs/configuration/analytics.mdx +22 -1
  181. package/docs/configuration/assistant.mdx +188 -36
  182. package/docs/configuration/consent.mdx +100 -0
  183. package/docs/configuration/customization.mdx +6 -4
  184. package/docs/configuration/index.mdx +63 -5
  185. package/docs/configuration/meta.ts +3 -0
  186. package/docs/configuration/narration.mdx +119 -0
  187. package/docs/configuration/rate-limiting.mdx +69 -0
  188. package/docs/configuration/search.mdx +30 -2
  189. package/docs/configuration/theming.mdx +3 -1
  190. package/docs/content/components.mdx +107 -1
  191. package/docs/content/frontmatter.mdx +77 -1
  192. package/docs/content/i18n.mdx +40 -3
  193. package/docs/content/includes.mdx +16 -0
  194. package/docs/content/index.mdx +4 -2
  195. package/docs/content/islands.mdx +1 -1
  196. package/docs/content/meta.mdx +4 -1
  197. package/docs/content/meta.ts +1 -0
  198. package/docs/content/navigation.mdx +28 -6
  199. package/docs/content/sources/contentful.mdx +32 -0
  200. package/docs/content/sources/custom.mdx +39 -0
  201. package/docs/content/sources/github-releases.mdx +31 -0
  202. package/docs/content/sources/index.mdx +91 -0
  203. package/docs/content/sources/meta.ts +16 -0
  204. package/docs/content/sources/notion.mdx +32 -0
  205. package/docs/content/sources/obsidian.mdx +37 -0
  206. package/docs/content/sources/payload.mdx +30 -0
  207. package/docs/content/sources/remote-mdx.mdx +40 -0
  208. package/docs/content/sources/sanity.mdx +29 -0
  209. package/docs/content/sources/strapi.mdx +31 -0
  210. package/docs/content/syntax.mdx +81 -2
  211. package/docs/content/variables.mdx +41 -0
  212. package/docs/content/versioning.mdx +1 -1
  213. package/docs/discoverability/agent-discovery.mdx +30 -3
  214. package/docs/discoverability/index.mdx +3 -2
  215. package/docs/discoverability/llms-txt.mdx +1 -1
  216. package/docs/discoverability/markdown.mdx +2 -2
  217. package/docs/discoverability/metadata.mdx +17 -0
  218. package/docs/discoverability/open-graph.mdx +5 -3
  219. package/docs/discoverability/rss.mdx +1 -1
  220. package/docs/references/api-pages.mdx +74 -0
  221. package/docs/references/asyncapi.mdx +2 -2
  222. package/docs/references/graphql.mdx +3 -3
  223. package/docs/references/meta.ts +1 -1
  224. package/docs/references/openapi.mdx +106 -5
  225. package/docs/references/scalar.mdx +3 -1
  226. package/package.json +36 -1
  227. package/skills/blume/SKILL.md +3 -0
  228. package/skills/blume-migrate/SKILL.md +5 -5
  229. package/skills/blume-migrate/references/docusaurus.md +3 -3
  230. package/skills/blume-migrate/references/fumadocs.md +1 -1
  231. package/skills/blume-migrate/references/mintlify.md +37 -19
  232. package/skills/blume-migrate/references/nextra.md +3 -3
  233. package/skills/blume-migrate/references/starlight.md +2 -1
  234. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +67 -16
  235. package/skills/blume-write-skill/SKILL.md +82 -0
  236. package/src/ai/agent-readability.ts +4 -3
  237. package/src/ai/agent-surface.ts +9 -7
  238. package/src/ai/ai-catalog.ts +12 -5
  239. package/src/ai/api/handlers.ts +2 -2
  240. package/src/ai/api/spec.ts +2 -2
  241. package/src/ai/api-catalog.ts +6 -2
  242. package/src/ai/ask-context.ts +73 -20
  243. package/src/ai/ask-data.ts +6 -0
  244. package/src/ai/ask-tools.ts +142 -0
  245. package/src/ai/ask.ts +354 -76
  246. package/src/ai/changelog-markdown.ts +2 -2
  247. package/src/ai/component-markdown.ts +75 -0
  248. package/src/ai/cors.ts +2 -0
  249. package/src/ai/index.ts +12 -0
  250. package/src/ai/link-headers.ts +14 -5
  251. package/src/ai/llms.ts +24 -9
  252. package/src/ai/markdown.ts +2 -1
  253. package/src/ai/mcp/data.ts +6 -0
  254. package/src/ai/mcp/discovery.ts +2 -2
  255. package/src/ai/mcp/query.ts +2 -2
  256. package/src/ai/mcp/server.ts +2 -2
  257. package/src/ai/openapi-components.ts +31 -9
  258. package/src/ai/relative-links.ts +42 -26
  259. package/src/ai/serializers.ts +2 -1
  260. package/src/ai/site-skill.ts +274 -0
  261. package/src/ai/skills.ts +49 -3
  262. package/src/ai/tar.ts +139 -12
  263. package/src/ai/write-skill.ts +97 -0
  264. package/src/analytics/head.ts +4 -0
  265. package/src/analytics/index.ts +5 -0
  266. package/src/analytics/one-dollar-stats.ts +86 -0
  267. package/src/analytics/schema.ts +2 -0
  268. package/src/astro/generate.ts +133 -19
  269. package/src/astro/include-hmr.ts +31 -12
  270. package/src/astro/include-refresh.ts +19 -8
  271. package/src/astro/index.ts +1 -0
  272. package/src/astro/integration.ts +143 -14
  273. package/src/astro/module-types.ts +6 -0
  274. package/src/astro/runtime-modules.ts +28 -13
  275. package/src/astro/templates.ts +364 -67
  276. package/src/astro/variables.ts +31 -0
  277. package/src/audit/catalog.ts +19 -0
  278. package/src/audit/checks/contrast.ts +183 -0
  279. package/src/audit/checks/i18n.ts +2 -3
  280. package/src/audit/checks/indexability.ts +2 -3
  281. package/src/audit/checks/links.ts +13 -13
  282. package/src/audit/checks/sitemap.ts +2 -3
  283. package/src/audit/graph.ts +3 -1
  284. package/src/audit/redirects.ts +46 -15
  285. package/src/audit/run.ts +14 -9
  286. package/src/audit/snapshot.ts +5 -0
  287. package/src/audit/types.ts +2 -1
  288. package/src/blume-modules.d.ts +7 -0
  289. package/src/captcha/client.ts +152 -0
  290. package/src/captcha/hcaptcha.ts +36 -0
  291. package/src/captcha/index.ts +28 -0
  292. package/src/captcha/schema.ts +32 -0
  293. package/src/captcha/turnstile.ts +38 -0
  294. package/src/captcha/verify.ts +71 -0
  295. package/src/cli/args.ts +32 -0
  296. package/src/cli/command-meta.ts +5 -0
  297. package/src/cli/commands/audit.ts +4 -1
  298. package/src/cli/commands/doctor.ts +25 -17
  299. package/src/cli/commands/eval.ts +19 -11
  300. package/src/cli/commands/skill.ts +96 -0
  301. package/src/cli/commands/translate.ts +9 -12
  302. package/src/cli/commands/validate.ts +3 -1
  303. package/src/cli/dev-lock.ts +157 -37
  304. package/src/cli/index.ts +5 -0
  305. package/src/cli/report-format.ts +11 -6
  306. package/src/cli/required-secrets.ts +71 -29
  307. package/src/components/BlumePage.astro +2 -0
  308. package/src/components/colors.ts +19 -0
  309. package/src/components/content/ApiEndpoint.astro +18 -0
  310. package/src/components/content/ApiField.astro +79 -0
  311. package/src/components/content/ApiPlayground.astro +62 -0
  312. package/src/components/content/ApiRail.astro +19 -0
  313. package/src/components/content/Badge.astro +5 -3
  314. package/src/components/content/Color.astro +1 -0
  315. package/src/components/content/Component.astro +2 -2
  316. package/src/components/content/FileTree.astro +1 -0
  317. package/src/components/content/GithubInfo.astro +1 -0
  318. package/src/components/content/Panel.astro +1 -0
  319. package/src/components/content/ParamField.astro +25 -0
  320. package/src/components/content/RequestExample.astro +17 -0
  321. package/src/components/content/ResponseExample.astro +17 -0
  322. package/src/components/content/ResponseField.astro +32 -0
  323. package/src/components/content/Step.astro +1 -0
  324. package/src/components/content/Tab.astro +0 -1
  325. package/src/components/content/Tabs.astro +4 -0
  326. package/src/components/content/Tile.astro +1 -0
  327. package/src/components/content/Tooltip.astro +1 -0
  328. package/src/components/content/Tree.astro +1 -0
  329. package/src/components/content/TypeTable.astro +4 -1
  330. package/src/components/content/View.astro +21 -0
  331. package/src/components/content/ViewSwitcher.astro +168 -0
  332. package/src/components/content/api-field.ts +100 -0
  333. package/src/components/content/api-page.ts +224 -0
  334. package/src/components/content/badge-color.ts +5 -2
  335. package/src/components/content/base-href.ts +13 -36
  336. package/src/components/content/view-switcher.ts +71 -0
  337. package/src/components/index.ts +2 -0
  338. package/src/components/islands/Assistant.astro +10 -1
  339. package/src/components/islands/assistant.tsx +125 -18
  340. package/src/components/islands/base-path.ts +47 -10
  341. package/src/components/islands/code-question.ts +62 -0
  342. package/src/components/islands/hooks.ts +145 -40
  343. package/src/components/islands/support-link.ts +68 -0
  344. package/src/components/islands/webmcp.ts +4 -2
  345. package/src/components/layout/Analytics.astro +11 -2
  346. package/src/components/layout/Banner.astro +14 -10
  347. package/src/components/layout/Breadcrumbs.astro +11 -4
  348. package/src/components/layout/ConsentBanner.astro +62 -0
  349. package/src/components/layout/ConsentHead.astro +38 -0
  350. package/src/components/layout/Directory.astro +94 -0
  351. package/src/components/layout/DiscoveryLinks.astro +9 -5
  352. package/src/components/layout/DrawerSwitchers.astro +51 -0
  353. package/src/components/layout/Empty.astro +2 -2
  354. package/src/components/layout/Header.astro +124 -82
  355. package/src/components/layout/LanguageSwitcher.astro +50 -17
  356. package/src/components/layout/NarrationPlayer.astro +1085 -0
  357. package/src/components/layout/NavSelector.astro +35 -9
  358. package/src/components/layout/NavTabMenu.astro +3 -3
  359. package/src/components/layout/NavTree.astro +16 -6
  360. package/src/components/layout/PageActions.astro +4 -3
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +78 -9
  363. package/src/components/layout/Pagination.astro +3 -3
  364. package/src/components/layout/ReferenceLayout.astro +17 -6
  365. package/src/components/layout/RelatedPages.astro +42 -0
  366. package/src/components/layout/RootLayout.astro +313 -50
  367. package/src/components/layout/Search.astro +64 -30
  368. package/src/components/layout/SiteFooter.astro +124 -0
  369. package/src/components/layout/SiteMetatags.astro +16 -0
  370. package/src/components/layout/VersionBanner.astro +2 -2
  371. package/src/components/layout/analytics-client.ts +12 -0
  372. package/src/components/layout/consent/native.ts +79 -0
  373. package/src/components/layout/consent/types.ts +37 -0
  374. package/src/components/layout/footer-links.ts +53 -0
  375. package/src/components/layout/footer-socials.ts +87 -0
  376. package/src/components/layout/locale-redirect.ts +107 -0
  377. package/src/components/layout/nav-utils.ts +83 -0
  378. package/src/components/layout/page-feedback.ts +96 -0
  379. package/src/components/layout/search/analytics.ts +98 -0
  380. package/src/components/layout/search/flexsearch.ts +17 -5
  381. package/src/components/layout/search/orama-cloud.ts +26 -14
  382. package/src/components/layout/search/types.ts +20 -0
  383. package/src/components/layout/search/typesense.ts +5 -1
  384. package/src/components/layout/toc-active.ts +41 -0
  385. package/src/components/layout/toc-element.ts +8 -14
  386. package/src/components/openapi/ApiTagOperations.astro +2 -2
  387. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  388. package/src/components/openapi/GraphqlChip.astro +2 -2
  389. package/src/components/openapi/Operation.astro +163 -13
  390. package/src/components/openapi/Playground.astro +4 -4
  391. package/src/components/openapi/RequestBody.astro +6 -4
  392. package/src/components/openapi/RequestPanel.astro +63 -8
  393. package/src/components/openapi/Responses.astro +6 -4
  394. package/src/components/openapi/SchemaTable.astro +7 -0
  395. package/src/components/openapi/async-snippets.ts +4 -4
  396. package/src/components/openapi/async.ts +38 -6
  397. package/src/components/openapi/code-samples.ts +97 -0
  398. package/src/components/openapi/helpers.ts +19 -9
  399. package/src/components/openapi/message-composer.ts +6 -1
  400. package/src/components/openapi/message.ts +17 -2
  401. package/src/components/openapi/operation-model.ts +81 -11
  402. package/src/components/openapi/panel.ts +4 -2
  403. package/src/components/openapi/param-style.ts +181 -0
  404. package/src/components/openapi/playground-client.ts +73 -16
  405. package/src/components/openapi/request.ts +190 -26
  406. package/src/components/openapi/schema-tree.ts +30 -24
  407. package/src/components/openapi/security.ts +6 -3
  408. package/src/components/openapi/snippets.ts +953 -13
  409. package/src/components/openapi/ws-client.ts +18 -2
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/base-path.ts +62 -16
  421. package/src/core/builtin-tags.ts +5 -0
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/config-input.ts +203 -27
  424. package/src/core/data.ts +44 -4
  425. package/src/core/diagnostics.ts +5 -2
  426. package/src/core/directive-diagnostics.ts +99 -0
  427. package/src/core/footer.ts +26 -0
  428. package/src/core/frontmatter.ts +21 -18
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/heading-markers.ts +5 -18
  431. package/src/core/i18n-ui.ts +82 -2
  432. package/src/core/i18n.ts +31 -3
  433. package/src/core/includes.ts +20 -3
  434. package/src/core/last-modified.ts +25 -3
  435. package/src/core/links.ts +16 -2
  436. package/src/core/locale-links.ts +5 -1
  437. package/src/core/localizable.ts +26 -0
  438. package/src/core/meta.ts +33 -19
  439. package/src/core/navigation.ts +71 -20
  440. package/src/core/page-modes.ts +56 -0
  441. package/src/core/project-graph.ts +138 -10
  442. package/src/core/redirect-patterns.ts +491 -0
  443. package/src/core/related.ts +131 -0
  444. package/src/core/schema.ts +342 -68
  445. package/src/core/sources/contentful-rich-text.ts +27 -20
  446. package/src/core/sources/filesystem.ts +20 -2
  447. package/src/core/sources/github-releases.ts +25 -6
  448. package/src/core/sources/lexical.ts +23 -18
  449. package/src/core/sources/lower.ts +201 -34
  450. package/src/core/sources/mdx-remote.ts +51 -16
  451. package/src/core/sources/normalize.ts +327 -91
  452. package/src/core/sources/notion.ts +52 -25
  453. package/src/core/sources/obsidian.ts +23 -6
  454. package/src/core/sources/portable-text.ts +38 -21
  455. package/src/core/sources/read.ts +8 -3
  456. package/src/core/sources/strapi-blocks.ts +20 -16
  457. package/src/core/sources/watch.ts +20 -7
  458. package/src/core/standard-schema.ts +10 -6
  459. package/src/core/types.ts +1 -1
  460. package/src/core/ui-packs/ar.ts +43 -0
  461. package/src/core/ui-packs/bg.ts +45 -0
  462. package/src/core/ui-packs/bn.ts +43 -0
  463. package/src/core/ui-packs/ca.ts +46 -0
  464. package/src/core/ui-packs/cs.ts +44 -0
  465. package/src/core/ui-packs/da.ts +45 -0
  466. package/src/core/ui-packs/de.ts +46 -0
  467. package/src/core/ui-packs/el.ts +45 -0
  468. package/src/core/ui-packs/es.ts +46 -0
  469. package/src/core/ui-packs/fa.ts +45 -0
  470. package/src/core/ui-packs/fi.ts +45 -0
  471. package/src/core/ui-packs/fr.ts +46 -0
  472. package/src/core/ui-packs/he.ts +43 -0
  473. package/src/core/ui-packs/hi.ts +44 -0
  474. package/src/core/ui-packs/hr.ts +45 -0
  475. package/src/core/ui-packs/hu.ts +44 -0
  476. package/src/core/ui-packs/id.ts +45 -0
  477. package/src/core/ui-packs/it.ts +45 -0
  478. package/src/core/ui-packs/ja.ts +45 -0
  479. package/src/core/ui-packs/ko.ts +44 -0
  480. package/src/core/ui-packs/nl.ts +46 -0
  481. package/src/core/ui-packs/no.ts +44 -0
  482. package/src/core/ui-packs/pl.ts +45 -0
  483. package/src/core/ui-packs/pt-br.ts +46 -0
  484. package/src/core/ui-packs/pt.ts +45 -0
  485. package/src/core/ui-packs/ro.ts +45 -0
  486. package/src/core/ui-packs/ru.ts +45 -0
  487. package/src/core/ui-packs/sk.ts +44 -0
  488. package/src/core/ui-packs/sr.ts +45 -0
  489. package/src/core/ui-packs/sv.ts +44 -0
  490. package/src/core/ui-packs/th.ts +43 -0
  491. package/src/core/ui-packs/tr.ts +45 -0
  492. package/src/core/ui-packs/uk.ts +45 -0
  493. package/src/core/ui-packs/vi.ts +45 -0
  494. package/src/core/ui-packs/zh-tw.ts +43 -0
  495. package/src/core/ui-packs/zh.ts +43 -0
  496. package/src/core/variables.ts +103 -0
  497. package/src/core/version-cut.ts +52 -15
  498. package/src/deploy/artifacts.ts +104 -14
  499. package/src/deploy/cloudflare-negotiation.ts +39 -17
  500. package/src/deploy/headers.ts +10 -5
  501. package/src/deploy/node-headers.ts +64 -10
  502. package/src/deploy/platforms/cloudflare.ts +44 -3
  503. package/src/deploy/platforms/netlify.ts +34 -1
  504. package/src/deploy/platforms/node.ts +1 -0
  505. package/src/deploy/platforms/static.ts +1 -0
  506. package/src/deploy/platforms/types.ts +8 -0
  507. package/src/deploy/platforms/vercel.ts +38 -2
  508. package/src/deploy/redirects.ts +139 -36
  509. package/src/deploy/robots.ts +2 -2
  510. package/src/deploy/rss.ts +4 -3
  511. package/src/deploy/sitemap.ts +7 -5
  512. package/src/deploy/vercel-negotiation.ts +36 -0
  513. package/src/markdown/api-rail.ts +259 -0
  514. package/src/markdown/base-links.ts +34 -36
  515. package/src/markdown/code-title.ts +35 -9
  516. package/src/markdown/directives.ts +242 -36
  517. package/src/markdown/features.ts +20 -2
  518. package/src/markdown/fence-meta.ts +2 -0
  519. package/src/markdown/include.ts +11 -1
  520. package/src/markdown/index.ts +35 -11
  521. package/src/markdown/math.ts +4 -4
  522. package/src/markdown/mdast.ts +5 -2
  523. package/src/markdown/relative-links.ts +12 -4
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +136 -11
  531. package/src/og/derive.ts +41 -31
  532. package/src/og/index.ts +1 -0
  533. package/src/openapi/asyncapi.ts +4 -2
  534. package/src/openapi/model.ts +77 -36
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +55 -2
  537. package/src/openapi/proxy.ts +63 -10
  538. package/src/openapi/references.ts +21 -5
  539. package/src/openapi/render-mdx.ts +46 -3
  540. package/src/openapi/scalar.ts +40 -6
  541. package/src/openapi/source.ts +1 -1
  542. package/src/ratelimit/cloudflare.ts +54 -0
  543. package/src/ratelimit/index.ts +30 -0
  544. package/src/ratelimit/memory.ts +45 -0
  545. package/src/ratelimit/runtime.ts +229 -0
  546. package/src/ratelimit/schema.ts +28 -0
  547. package/src/ratelimit/upstash.ts +30 -0
  548. package/src/ratelimit/wrangler.ts +70 -0
  549. package/src/reference/asyncapi.ts +2 -2
  550. package/src/reference/graphql.ts +5 -2
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +51 -7
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +201 -28
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/version-scope.ts +30 -0
  557. package/src/search/documents.ts +93 -28
  558. package/src/search/orama-index.ts +28 -3
  559. package/src/search/pagefind-ranking.ts +39 -0
  560. package/src/search/popular.ts +2 -1
  561. package/src/search/sync/algolia.ts +15 -4
  562. package/src/search/sync/orama-cloud.ts +3 -0
  563. package/src/search/sync/typesense.ts +5 -0
  564. package/src/seo/jsonld.ts +7 -3
  565. package/src/seo/metatags.ts +67 -0
  566. package/src/theme/chrome-icons.ts +1 -0
  567. package/src/theme/color.ts +500 -0
  568. package/src/theme/entry.ts +56 -3
  569. package/src/theme/palette.ts +121 -45
  570. package/src/translate/meta.ts +68 -24
  571. package/src/translate/prompts.ts +1 -0
  572. package/src/translate/run.ts +3 -3
  573. package/src/translate/validate.ts +12 -3
  574. package/src/translate/work-list.ts +56 -17
  575. package/src/upgrade/upgrade.ts +4 -171
  576. package/dist/cli/chunk-3yce002v.js.map +0 -14
  577. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  578. package/dist/cli/chunk-6dtt0zfn.js.map +0 -10
  579. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  580. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  581. package/dist/cli/chunk-esh98wmb.js +0 -23
  582. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  583. package/dist/cli/chunk-hqp2ajnh.js.map +0 -35
  584. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  585. package/dist/cli/chunk-j85scx15.js.map +0 -10
  586. package/dist/cli/chunk-mqc662a6.js +0 -75
  587. package/dist/cli/chunk-mqc662a6.js.map +0 -11
  588. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  589. package/dist/cli/chunk-qkb5a8sa.js.map +0 -10
  590. package/dist/cli/chunk-v6ya5kcb.js.map +0 -189
  591. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  592. package/dist/cli/chunk-xh43dwgw.js.map +0 -36
  593. package/docs/content/sources.mdx +0 -338
  594. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-1n228xqb.js.map} +0 -0
  595. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-5ffywsmb.js.map} +0 -0
  596. /package/dist/cli/{chunk-g698a744.js.map → chunk-5wj2qwn2.js.map} +0 -0
  597. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-amrb6hgh.js.map} +0 -0
  598. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-gp84za0h.js.map} +0 -0
  599. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-p804f3pe.js.map} +0 -0
  600. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-ta27feev.js.map} +0 -0
  601. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-y4ykbark.js.map} +0 -0
@@ -1,8 +1,11 @@
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
 
8
+ import { MARKDOWN_FEATURES } from "../../markdown/features.ts";
6
9
  import { mountBasePath } from "../base-path.ts";
7
10
  import { nextFenceState } from "../code-fences.ts";
8
11
  import type { FenceState } from "../code-fences.ts";
@@ -11,6 +14,7 @@ import { occupySlug, parseHeadingMarkers } from "../heading-markers.ts";
11
14
  import { localePlacement, localizeRoute } from "../i18n.ts";
12
15
  import { titleWord } from "../navigation.ts";
13
16
  import { stripOrderingPrefix } from "../ordering-prefix.ts";
17
+ import { relatedPageLinks } from "../related.ts";
14
18
  import { pageMetaSchema } from "../schema.ts";
15
19
  import type {
16
20
  FrontmatterExtend,
@@ -33,6 +37,14 @@ const stripNumericPrefix = (segment: string): string =>
33
37
  const groupLabel = (segment: string): string | null =>
34
38
  segment.match(GROUP_FOLDER)?.groups?.label ?? null;
35
39
 
40
+ /**
41
+ * {@link groupLabel} for a file or folder name, which may carry its ordering
42
+ * prefix outside the parentheses: `01-(guides)` is the `guides` group, sorted
43
+ * first, the way `01-guides` is the `guides` folder.
44
+ */
45
+ const orderedGroupLabel = (segment: string): string | null =>
46
+ groupLabel(segment) ?? groupLabel(stripNumericPrefix(segment));
47
+
36
48
  /**
37
49
  * Slugify a content/route slug (Sanity, Notion, frontmatter `slug`). Heading
38
50
  * anchor ids are *not* slugged here — they use a `github-slugger` in
@@ -74,11 +86,46 @@ const titleCase = (value: string): string =>
74
86
  * scheme (`Guide: Architecture.md` → `guide:`), which crashes Astro's
75
87
  * prerender write with "The URL must be of scheme file"; control characters
76
88
  * (an embedded newline in a filename) are silently dropped by the URL parser,
77
- * desyncing the route from its output path. Both are legal in macOS/Linux
78
- * filenames, so they are removed here rather than rejected.
89
+ * desyncing the route from its output path. `#` and `?` start a URL's
90
+ * fragment and query, so an unescaped `/sdks/c#` link lands on `/sdks/c`
91
+ * while Astro writes the page to `c%23/`; a `%` starts an escape, and a bare
92
+ * one is an invalid URL Astro can't decode (and one it escapes, `%25`, finds
93
+ * no static path). All are legal in macOS/Linux filenames, so they are
94
+ * removed here rather than rejected: `100%.md` publishes at `/100`, and a
95
+ * slug of `sdks/c#` at `/sdks/c`. A file whose own path holds `#` or `?`
96
+ * never gets this far — Astro can't load it (see
97
+ * {@link unloadablePathDiagnostic}).
79
98
  */
80
99
  const sanitizeSegment = (segment: string): string =>
81
- segment.replaceAll(/[:\p{Cc}]/gu, "");
100
+ segment.replaceAll(/[:#?%\p{Cc}]/gu, "");
101
+
102
+ // Astro's content loader reads each entry at `new URL("./" + encodeURI(entry),
103
+ // base)`. `encodeURI` escapes `%` but leaves `#` and `?` alone, so in a path
104
+ // they start the URL's fragment and query, and the read misses the file.
105
+ const UNLOADABLE_PATH = /[#?]/u;
106
+
107
+ /**
108
+ * The error for a content file Astro's content loader can't read, or
109
+ * undefined when it can. A `#` or `?` anywhere in the path the loader is
110
+ * handed (`sdks/c#.md`, `faq/why?.md`) truncates the file URL it reads
111
+ * through, so the read fails with ENOENT and the page renders "Page not
112
+ * found" at its route. A source leaves such a file out of its scan and
113
+ * reports this instead of publishing a route that can never render.
114
+ */
115
+ export const unloadablePathDiagnostic = (
116
+ path: string,
117
+ file: string
118
+ ): Diagnostic | undefined =>
119
+ UNLOADABLE_PATH.test(path)
120
+ ? {
121
+ code: "BLUME_UNLOADABLE_FILE_NAME",
122
+ file,
123
+ 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.`,
124
+ severity: "error",
125
+ suggestion:
126
+ 'Rename the file (or its folder) without "#" or "?". Both are dropped from the page\'s URL anyway, so the page keeps its route.',
127
+ }
128
+ : undefined;
82
129
 
83
130
  /**
84
131
  * Fold one raw path part into the accumulating route segments/groups.
@@ -95,7 +142,7 @@ const addRouteSegment = (
95
142
  if (part === "") {
96
143
  return;
97
144
  }
98
- const group = groupLabel(part);
145
+ const group = ordered ? orderedGroupLabel(part) : groupLabel(part);
99
146
  if (group !== null) {
100
147
  groups.push(group);
101
148
  return;
@@ -166,7 +213,6 @@ const SETEXT_UNDERLINE = /^ {0,3}(?<marker>=+|-+)\s*$/u;
166
213
  const PARAGRAPH_INTERRUPT = /^ {0,3}(?:[-+*][ \t]|\d{1,9}[.)][ \t]|>)/u;
167
214
  const THEMATIC_BREAK =
168
215
  /^ {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$/u;
169
- const FRONT_MATTER_CLOSE = /^(?:-{3}|\.{3})\s*$/u;
170
216
  // `<Prompt>` renders its children into a permanently `hidden` DOM node (see
171
217
  // `Prompt.astro`) — the agent-facing prompt text is never visible page
172
218
  // content, only read by client JS for the copy button. Any `##` inside it
@@ -183,41 +229,13 @@ const PROMPT_OPEN = /^<Prompt(?![\w-])/u;
183
229
  // children text (`...copy this.</Prompt>`), not just sit on its own line.
184
230
  const PROMPT_CLOSE = /<\/Prompt>/u;
185
231
 
186
- /**
187
- * The body lines, minus a leading front matter block, plus the height of the
188
- * block that was dropped (`offset`) so line numbers can be reported against
189
- * the whole body. Bodies from the normalize pipeline are already
190
- * frontmatter-stripped, but `scanBody` also runs on raw documents — where a
191
- * leading `---` block (closed by `---` or `...`) is front matter, not a
192
- * thematic break whose closing `---` would underline the last metadata line
193
- * into a phantom setext heading.
194
- */
195
- const linesWithoutFrontMatter = (body: string) => {
196
- const lines = body.split("\n");
197
- if (!/^-{3}\s*$/u.test(lines[0] ?? "")) {
198
- return { lines, offset: 0 };
199
- }
200
- // A blank line directly after the dashes means the body *opens* with a
201
- // thematic break, not front matter — YAML metadata starts on the very next
202
- // line. Treating it as an unclosed block ate everything up to the next
203
- // `---`/`...` line of an already-stripped body.
204
- if ((lines[1] ?? "").trim() === "") {
205
- return { lines, offset: 0 };
206
- }
207
- const close = lines.findIndex(
208
- (line, index) => index > 0 && FRONT_MATTER_CLOSE.test(line)
209
- );
210
- return close === -1
211
- ? { lines, offset: 0 }
212
- : { lines: lines.slice(close + 1), offset: close + 1 };
213
- };
214
-
215
232
  // A trailing `{#id}` heading marker written without a backslash escape. Both
216
- // pipelines resolve escapes before parsing markers (see `ESCAPED_PUNCTUATION`),
217
- // so `\{#id\}` is the same marker — and the only spelling that survives the
218
- // MDX parser, where a bare `{…}` is a JSX expression and `#id` is not a valid
219
- // one (`Could not parse expression with acorn`). Further bracket markers may
220
- // follow the brace (`{#id} [toc]`), nothing else.
233
+ // pipelines resolve escapes before parsing markers (the scan parses each
234
+ // heading with the renderer's grammar — see `renderHeading`), so `\{#id\}` is
235
+ // the same marker — and the only spelling that survives the MDX parser, where
236
+ // a bare `{…}` is a JSX expression and `#id` is not a valid one (`Could not
237
+ // parse expression with acorn`). Further bracket markers may follow the brace
238
+ // (`{#id} [toc]`), nothing else.
221
239
  const BARE_CURLY_MARKER =
222
240
  /(?<!\\)\{#(?<id>[^\s}]+)\}(?:\s*\[(?:#[^\s\]]+|!?toc)\])*\s*$/u;
223
241
 
@@ -311,12 +329,31 @@ const finishPromptTag = (
311
329
  * collapses `--`; github-slugger keeps it) and resolves repeated headings the
312
330
  * same way (`setup`, `setup-1`).
313
331
  */
314
- // CommonMark's escapable ASCII punctuation. The renderer only ever sees
315
- // heading text *after* the Markdown parser has resolved backslash escapes, so
316
- // `\[toc]` reaches the hast as plain `[toc]` and the marker still applies;
317
- // resolving escapes here keeps the two pipelines identical (there is no
318
- // inline way to write a literal trailing marker — use inline code instead).
319
- const ESCAPED_PUNCTUATION = /\\(?<char>[!-/:-@[-`{-~])/gu;
332
+ // A heading's inline Markdown is parsed with the renderer's own grammar:
333
+ // Blume's `.md` feature set plus the Astro defaults Blume never turns off, GFM
334
+ // and smart punctuation (Astro's `smartypants`). Front matter is off — the
335
+ // source is a lone heading. The display text is read without smart
336
+ // punctuation (see `toHeading`).
337
+ const HEADING_FEATURES = {
338
+ ...MARKDOWN_FEATURES,
339
+ frontmatter: false,
340
+ gfm: true,
341
+ };
342
+ const HEADING_PARSE = {
343
+ features: { ...HEADING_FEATURES, smartPunctuation: true },
344
+ };
345
+ const HEADING_TEXT_PARSE = {
346
+ features: { ...HEADING_FEATURES, smartPunctuation: false },
347
+ };
348
+
349
+ // Characters that can make a heading's rendered text differ from its source:
350
+ // escapes, code spans, emphasis/strikethrough/sub/superscript markers, link and
351
+ // image brackets, raw HTML and autolinks, entities, and the quotes, dashes, and
352
+ // ellipses smart punctuation rewrites. A heading with none of them renders as
353
+ // written, so it skips the parse.
354
+ const INLINE_MARKUP = /[\\`*_~^[\]<&'"]|--|\.\.\./u;
355
+ // The subset smart punctuation rewrites (`"a"` → `“a”`, `--` → `–`, `...` → `…`).
356
+ const SMART_PUNCTUATION = /['"]|--|\.\.\./u;
320
357
 
321
358
  // The start of a link-reference definition, as the renderer accepts it:
322
359
  // `[label]:` after up to 3 leading spaces, optionally inside block-quote or
@@ -334,10 +371,13 @@ const REF_DEFINITION =
334
371
 
335
372
  /**
336
373
  * The normalized labels of every link-reference definition in the body
337
- * (outside fenced code). A trailing heading bracket whose label is defined is
338
- * a CommonMark shortcut link, not a marker — the renderer leaves it in the
339
- * heading as an `<a>`, so the marker parse must skip it too. Labels match
340
- * case-insensitively with collapsed internal whitespace (CommonMark).
374
+ * (outside fenced code), footnote definitions (`^1`) included. A heading
375
+ * bracket whose label is defined is a CommonMark reference link —
376
+ * `[text][label]`, or a shortcut `[label]` that would otherwise read as a
377
+ * trailing `[toc]`/`[#id]` marker — or a GFM footnote reference, so each
378
+ * heading is parsed together with the definitions it names (see
379
+ * {@link definitionsFor}). Labels match case-insensitively with collapsed
380
+ * internal whitespace (CommonMark).
341
381
  *
342
382
  * Only a *valid* definition defines a label: the label must contain a
343
383
  * non-whitespace character, and a destination must follow — on the same line
@@ -369,14 +409,179 @@ const refDefinitionLabels = (lines: readonly string[]): Set<string> => {
369
409
  };
370
410
 
371
411
  /**
372
- * A heading record from raw heading text: escapes resolved and trailing
373
- * markers stripped, exactly as the renderer sees them. A `[#custom-id]` pin
374
- * becomes the slug verbatim and — matching the renderer — occupies its id in
375
- * the slugger, so a later heading whose auto-slug collides disambiguates
376
- * (`setup` → `setup-1`). `[!toc]`/`[toc]` headings stay in the record: their
377
- * ids exist in the rendered page, so links to them are valid anchors
378
- * regardless of TOC visibility. A heading that is nothing but markers keeps
379
- * them as literal text, mirroring the renderer.
412
+ * The link-reference and footnote definitions a heading's brackets may name,
413
+ * as source lines to parse the heading with. A definition the heading doesn't
414
+ * use changes nothing, so matching is loose (the label anywhere in the folded
415
+ * text). A footnote label (`^1`) is defined too, so `[^1]` parses as the
416
+ * footnote reference it renders as, not as literal text.
417
+ */
418
+ const definitionsFor = (raw: string, labels: ReadonlySet<string>): string => {
419
+ if (!raw.includes("[")) {
420
+ return "";
421
+ }
422
+ const folded = raw.replaceAll(/\s+/gu, " ").toLowerCase();
423
+ let definitions = "";
424
+ for (const label of labels) {
425
+ if (folded.includes(label)) {
426
+ definitions += `\n\n[${label}]: /`;
427
+ }
428
+ }
429
+ return definitions;
430
+ };
431
+
432
+ /**
433
+ * The number each footnote renders as, keyed by identifier. GFM numbers
434
+ * footnotes in the order the body first references them, whatever order their
435
+ * definitions sit in, so a `[^b]` cited before `[^a]` renders as 1. A
436
+ * reference inside a footnote definition is left out: the renderer reaches
437
+ * those only after the body.
438
+ */
439
+ const footnoteNumbers = (body: string): Map<string, number> => {
440
+ const numbers = new Map<string, number>();
441
+ const visit = (nodes: readonly Nodes[]): void => {
442
+ for (const node of nodes) {
443
+ if (node.type === "footnoteReference") {
444
+ if (!numbers.has(node.identifier)) {
445
+ numbers.set(node.identifier, numbers.size + 1);
446
+ }
447
+ } else if (node.type !== "footnoteDefinition" && "children" in node) {
448
+ visit(node.children);
449
+ }
450
+ }
451
+ };
452
+ const tree = markdownToMdast(body, HEADING_PARSE);
453
+ visit("children" in tree ? tree.children : []);
454
+ return numbers;
455
+ };
456
+
457
+ /** What reading one heading needs from the rest of its body. */
458
+ interface HeadingContext {
459
+ /**
460
+ * The number each footnote renders as (see {@link footnoteNumbers}),
461
+ * computed on first use: only a heading that cites a footnote needs it.
462
+ */
463
+ footnotes: () => ReadonlyMap<string, number>;
464
+ /** The body's link-reference and footnote definition labels. */
465
+ labels: ReadonlySet<string>;
466
+ }
467
+
468
+ /**
469
+ * The text content of mdast inline nodes, the way the rendered heading reads
470
+ * it: link and emphasis text, code-span contents, decoded entities, and a
471
+ * footnote reference's number. Raw HTML tags add no text (the text between
472
+ * them is its own node), and an image's alt is an attribute rather than text
473
+ * content.
474
+ */
475
+ const inlineText = (nodes: readonly Nodes[], context: HeadingContext): string =>
476
+ nodes
477
+ .map((node) => {
478
+ if (
479
+ node.type === "html" ||
480
+ node.type === "image" ||
481
+ node.type === "imageReference"
482
+ ) {
483
+ return "";
484
+ }
485
+ if (node.type === "break") {
486
+ return "\n";
487
+ }
488
+ if (node.type === "footnoteReference") {
489
+ // A reference the body's own parse never numbered (a heading the scan
490
+ // finds inside an HTML block) is no footnote to the renderer either.
491
+ const number = context.footnotes().get(node.identifier);
492
+ return number === undefined
493
+ ? `[^${node.label ?? node.identifier}]`
494
+ : String(number);
495
+ }
496
+ if ("children" in node) {
497
+ return inlineText(node.children, context);
498
+ }
499
+ return "value" in node ? node.value : "";
500
+ })
501
+ .join("");
502
+
503
+ /** A heading's rendered text plus the trailing markers the renderer strips. */
504
+ interface RenderedHeading {
505
+ /** Author-pinned anchor id from `[#id]`/`{#id}`, used verbatim. */
506
+ id?: string;
507
+ /** The text content the renderer slugs, markers stripped, untrimmed. */
508
+ text: string;
509
+ toc?: "hide" | "only";
510
+ }
511
+
512
+ /** A heading whose source is its text: one text node, markers stripped. */
513
+ const plainHeading = (raw: string): RenderedHeading => {
514
+ const markers = parseHeadingMarkers(raw);
515
+ // A heading that is nothing but markers keeps them as literal text.
516
+ return markers.text === ""
517
+ ? { text: raw }
518
+ : { id: markers.id, text: markers.text, toc: markers.toc };
519
+ };
520
+
521
+ /**
522
+ * Parse a heading's inline Markdown and read it the way
523
+ * `markdown/heading-anchors` does: the heading's text content, with trailing
524
+ * markers stripped from its last node only when that node is text (a heading
525
+ * ending in inline code or a link has no marker position) and some heading
526
+ * text remains. Parsing (rather than regex-stripping) resolves escapes —
527
+ * `\[toc]` still reaches the marker parse as `[toc]` — and emphasis the way
528
+ * CommonMark does, so `snake_case` stays whole while `_note_` loses its
529
+ * underscores.
530
+ */
531
+ const renderHeading = (
532
+ raw: string,
533
+ form: "atx" | "setext",
534
+ context: HeadingContext,
535
+ parse: typeof HEADING_PARSE
536
+ ): RenderedHeading => {
537
+ if (!INLINE_MARKUP.test(raw)) {
538
+ return plainHeading(raw);
539
+ }
540
+ const source = form === "atx" ? `# ${raw}` : `${raw}\n=`;
541
+ const tree = markdownToMdast(
542
+ `${source}${definitionsFor(raw, context.labels)}`,
543
+ parse
544
+ );
545
+ const heading = "children" in tree ? tree.children.at(0) : undefined;
546
+ // The scan's paragraph tracking is coarser than the parser's: underlined
547
+ // text that opens with an HTML block or a definition is no heading to the
548
+ // renderer. It is read as written.
549
+ if (heading?.type !== "heading") {
550
+ return plainHeading(raw);
551
+ }
552
+ const { children } = heading;
553
+ const text = inlineText(children, context);
554
+ const last = children.at(-1);
555
+ if (last?.type !== "text") {
556
+ return { text };
557
+ }
558
+ const markers = parseHeadingMarkers(last.value);
559
+ const stripped = last.value.length - markers.text.length;
560
+ if (stripped === 0 || (markers.text === "" && children.length === 1)) {
561
+ return { text };
562
+ }
563
+ return {
564
+ id: markers.id,
565
+ text: text.slice(0, text.length - stripped),
566
+ toc: markers.toc,
567
+ };
568
+ };
569
+
570
+ /**
571
+ * A heading record from raw heading text, read exactly as the renderer reads
572
+ * it: inline Markdown reduced to its rendered text (see {@link renderHeading})
573
+ * and trailing markers stripped. The slug is the renderer's slug of that text.
574
+ * A `[#custom-id]` pin becomes the slug verbatim and — matching the renderer —
575
+ * occupies its id in the slugger, so a later heading whose auto-slug collides
576
+ * disambiguates (`setup` → `setup-1`). `[!toc]`/`[toc]` headings stay in the
577
+ * record: their ids exist in the rendered page, so links to them are valid
578
+ * anchors regardless of TOC visibility.
579
+ *
580
+ * The record's `text` — a page's fallback title and sidebar label — is the
581
+ * same rendered text with whitespace collapsed, but read without smart
582
+ * punctuation: it keeps the straight quotes and double hyphens the author
583
+ * typed, the way a frontmatter `title` does, and the way an Obsidian
584
+ * `[[Note#It's here]]` heading link spells the heading it resolves against.
380
585
  */
381
586
  /** A scanned heading plus whether its id came from an author pin. */
382
587
  interface ScannedHeading {
@@ -387,23 +592,23 @@ interface ScannedHeading {
387
592
  const toHeading = (
388
593
  depth: number,
389
594
  raw: string,
595
+ form: "atx" | "setext",
390
596
  slugger: GithubSlugger,
391
- isRefDefined: (label: string) => boolean
597
+ context: HeadingContext
392
598
  ): ScannedHeading => {
393
- const unescaped = raw.replaceAll(ESCAPED_PUNCTUATION, "$<char>");
394
- const markers = parseHeadingMarkers(unescaped, isRefDefined);
395
- const text = markers.text.trim();
396
- if (text === "" && (markers.id !== undefined || markers.toc !== undefined)) {
397
- return {
398
- heading: { depth, slug: slugger.slug(unescaped), text: unescaped },
399
- pinned: false,
400
- };
599
+ const rendered = renderHeading(raw, form, context, HEADING_PARSE);
600
+ const display = SMART_PUNCTUATION.test(raw)
601
+ ? renderHeading(raw, form, context, HEADING_TEXT_PARSE).text
602
+ : rendered.text;
603
+ const text = display.replaceAll(/[\t\n\f\r ]+/gu, " ").trim();
604
+ if (rendered.id !== undefined) {
605
+ occupySlug(slugger, rendered.id);
606
+ return { heading: { depth, slug: rendered.id, text }, pinned: true };
401
607
  }
402
- if (markers.id !== undefined) {
403
- occupySlug(slugger, markers.id);
404
- return { heading: { depth, slug: markers.id, text }, pinned: true };
405
- }
406
- return { heading: { depth, slug: slugger.slug(text), text }, pinned: false };
608
+ return {
609
+ heading: { depth, slug: slugger.slug(rendered.text), text },
610
+ pinned: false,
611
+ };
407
612
  };
408
613
 
409
614
  /** Record a heading and where a trailing marker would be written for it. */
@@ -435,7 +640,7 @@ const scanContentLine = (
435
640
  state: HeadingScanState,
436
641
  slugger: GithubSlugger,
437
642
  headings: Heading[],
438
- isRefDefined: (label: string) => boolean
643
+ context: HeadingContext
439
644
  ): void => {
440
645
  // Inline code is masked so a documented `<a id="…">` isn't an anchor.
441
646
  state.anchorLines.push(line.replaceAll(INLINE_CODE, ""));
@@ -446,7 +651,7 @@ const scanContentLine = (
446
651
  pushHeading(
447
652
  headings,
448
653
  state,
449
- toHeading(depth, text, slugger, isRefDefined),
654
+ toHeading(depth, text, "atx", slugger, context),
450
655
  state.line
451
656
  );
452
657
  noteCurlyMarker(text, state.line, state);
@@ -456,15 +661,17 @@ const scanContentLine = (
456
661
  const setext = line.match(SETEXT_UNDERLINE);
457
662
  if (setext?.groups && state.paragraph.length > 0) {
458
663
  // Setext wins over thematic break when it closes a paragraph (CommonMark);
459
- // a multi-line paragraph renders as one heading, soft breaks as spaces.
664
+ // a multi-line paragraph renders as one heading. Its lines keep their line
665
+ // breaks, which the rendered text content keeps too (and the slugger
666
+ // drops, so `Multi\nline` anchors as `multiline`).
460
667
  const depth = setext.groups.marker?.startsWith("=") ? 1 : 2;
461
- const text = state.paragraph.join(" ").trim();
668
+ const text = state.paragraph.join("\n");
462
669
  // A setext heading's markers trail its last text line, just above the
463
670
  // underline — that is where a pin is appended.
464
671
  pushHeading(
465
672
  headings,
466
673
  state,
467
- toHeading(depth, text, slugger, isRefDefined),
674
+ toHeading(depth, text, "setext", slugger, context),
468
675
  state.line - 1
469
676
  );
470
677
  noteCurlyMarker(text, state.paragraphStart, state);
@@ -533,7 +740,7 @@ const scanHeadingLine = (
533
740
  state: HeadingScanState,
534
741
  slugger: GithubSlugger,
535
742
  headings: Heading[],
536
- isRefDefined: (label: string) => boolean
743
+ context: HeadingContext
537
744
  ): void => {
538
745
  // Comments hide fences too, and fences and prompts hide comments. A comment
539
746
  // line still goes to the anchor pass, which strips HTML comments itself.
@@ -577,7 +784,7 @@ const scanHeadingLine = (
577
784
  state.paragraph = [];
578
785
  return;
579
786
  }
580
- scanContentLine(line, state, slugger, headings, isRefDefined);
787
+ scanContentLine(line, state, slugger, headings, context);
581
788
  };
582
789
 
583
790
  /** Where a heading's text ends in the scanned text, for appending a marker. */
@@ -607,7 +814,10 @@ export interface BodyScan {
607
814
  /**
608
815
  * Scan a body for its headings, explicit HTML anchors, and unescaped `{#id}`
609
816
  * markers in one fence-aware walk (the same walk `extractHeadings` exposes for
610
- * headings alone).
817
+ * headings alone). The body is the page's content with its front matter
818
+ * already stripped — the renderers read front matter off the page and never
819
+ * again from its body — so a leading `---` block is a thematic break and
820
+ * content, in `.md` and `.mdx` alike.
611
821
  */
612
822
  export const scanBody = (body: string): BodyScan => {
613
823
  const headings: Heading[] = [];
@@ -625,13 +835,18 @@ export const scanBody = (body: string): BodyScan => {
625
835
  sites: [],
626
836
  };
627
837
 
628
- const { lines, offset } = linesWithoutFrontMatter(body);
629
- const definedLabels = refDefinitionLabels(lines);
630
- const isRefDefined = (label: string): boolean =>
631
- definedLabels.has(label.toLowerCase());
838
+ const lines = body.split("\n");
839
+ let footnotes: Map<string, number> | undefined;
840
+ const context: HeadingContext = {
841
+ footnotes: () => {
842
+ footnotes ??= footnoteNumbers(lines.join("\n"));
843
+ return footnotes;
844
+ },
845
+ labels: refDefinitionLabels(lines),
846
+ };
632
847
  for (const [index, line] of lines.entries()) {
633
- state.line = index + offset + 1;
634
- scanHeadingLine(line, state, slugger, headings, isRefDefined);
848
+ state.line = index + 1;
849
+ scanHeadingLine(line, state, slugger, headings, context);
635
850
  }
636
851
 
637
852
  const anchors = new Set<string>();
@@ -961,7 +1176,7 @@ const deriveTitle = (
961
1176
  // content root's own index has no folder to borrow from.
962
1177
  const folder = stem === "index" ? parts.at(-2) : undefined;
963
1178
  if (folder !== undefined) {
964
- return titleCase(stripNumericPrefix(groupLabel(folder) ?? folder));
1179
+ return titleCase(stripNumericPrefix(orderedGroupLabel(folder) ?? folder));
965
1180
  }
966
1181
  return titleCase(stem);
967
1182
  };
@@ -1180,6 +1395,25 @@ const validateCustomKeys = (
1180
1395
  };
1181
1396
  };
1182
1397
 
1398
+ // A `.` or `..` path segment, which a browser resolves away before requesting.
1399
+ const DOT_SEGMENT = /(?:^|\/)\.{1,2}(?:\/|$)/u;
1400
+
1401
+ /**
1402
+ * A frontmatter `slug` with a `.` or `..` segment names no reachable URL: a
1403
+ * browser normalizes the link (`guides/./x` → `guides/x`, `../x` → `/x`)
1404
+ * before requesting it, and `..` would have Astro write the page outside the
1405
+ * build output. Rejected alongside the schema's own errors.
1406
+ */
1407
+ const slugIssues = (slug: string | undefined): CustomKeyIssue[] =>
1408
+ slug !== undefined && DOT_SEGMENT.test(slug)
1409
+ ? [
1410
+ {
1411
+ 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.`,
1412
+ path: ["slug"],
1413
+ },
1414
+ ]
1415
+ : [];
1416
+
1183
1417
  /**
1184
1418
  * Parse an entry's frontmatter: built-in keys through the strict page schema,
1185
1419
  * custom keys (`frontmatter.extend` plus the page type's
@@ -1218,8 +1452,12 @@ const parseEntryMeta = (
1218
1452
 
1219
1453
  const result = pageMetaSchema.safeParse(known);
1220
1454
  const customResult = extend ? validateCustomKeys(entry.data, extend) : null;
1455
+ const issues = [
1456
+ ...(result.success ? slugIssues(result.data.slug) : []),
1457
+ ...(customResult?.issues ?? []),
1458
+ ];
1221
1459
 
1222
- if (result.success && (customResult?.issues.length ?? 0) === 0) {
1460
+ if (result.success && issues.length === 0) {
1223
1461
  return { custom: customResult?.custom, meta: result.data };
1224
1462
  }
1225
1463
 
@@ -1239,9 +1477,7 @@ const parseEntryMeta = (
1239
1477
  return {
1240
1478
  diagnostics: [
1241
1479
  ...(result.success ? [] : diagnosticsFromZod(result.error, location)),
1242
- ...(customResult
1243
- ? diagnosticsFromIssues(customResult.issues, location)
1244
- : []),
1480
+ ...diagnosticsFromIssues(issues, location),
1245
1481
  ],
1246
1482
  };
1247
1483
  };
@@ -1319,7 +1555,7 @@ export const normalizeEntry = (
1319
1555
  id: `${ctx.source.name}:${entry.ref}`,
1320
1556
  includes: entryIncludes(entry),
1321
1557
  lastModified: meta.lastModified ?? entry.lastModified,
1322
- links: entryLinks(entry),
1558
+ links: [...entryLinks(entry), ...relatedPageLinks(meta.related)],
1323
1559
  meta,
1324
1560
  monolingual: ctx.source.monolingual,
1325
1561
  navPath,