blume 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
  3. package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
  4. package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
  5. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
  6. package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
  7. package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
  8. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
  9. package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
  10. package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
  11. package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
  12. package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
  13. package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
  14. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  15. package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
  16. package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
  17. package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
  18. package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
  19. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  20. package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
  21. package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
  22. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
  23. package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
  24. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  25. package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
  26. package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
  27. package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
  28. package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
  29. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  30. package/dist/cli/chunk-e04dxsz1.js +39 -0
  31. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  32. package/dist/cli/{chunk-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
  33. package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
  34. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  35. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  36. package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
  37. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  38. package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
  39. package/dist/cli/chunk-j85vccga.js.map +36 -0
  40. package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
  41. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
  42. package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
  43. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
  44. package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
  45. package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
  46. package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
  47. package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
  48. package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
  49. package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
  50. package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
  51. package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
  52. package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
  53. package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
  54. package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
  55. package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
  56. package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
  57. package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
  58. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  59. package/dist/cli/index.js +20 -18
  60. package/dist/cli/index.js.map +3 -3
  61. package/dist/types/ai/link-headers.d.ts +8 -1
  62. package/dist/types/ai/openapi-components.d.ts +5 -2
  63. package/dist/types/ai/relative-links.d.ts +9 -7
  64. package/dist/types/ai/skills.d.ts +4 -1
  65. package/dist/types/ai/tar.d.ts +1 -3
  66. package/dist/types/analytics/index.d.ts +2 -0
  67. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  68. package/dist/types/analytics/schema.d.ts +14 -0
  69. package/dist/types/astro/integration.d.ts +3 -2
  70. package/dist/types/core/base-path.d.ts +21 -7
  71. package/dist/types/core/config-input.d.ts +9 -7
  72. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  73. package/dist/types/core/heading-markers.d.ts +5 -7
  74. package/dist/types/core/i18n.d.ts +2 -0
  75. package/dist/types/core/last-modified.d.ts +10 -0
  76. package/dist/types/core/meta.d.ts +8 -0
  77. package/dist/types/core/schema.d.ts +7 -0
  78. package/dist/types/core/sources/lower.d.ts +29 -16
  79. package/dist/types/core/sources/normalize.d.ts +13 -1
  80. package/dist/types/core/sources/watch.d.ts +12 -5
  81. package/dist/types/core/standard-schema.d.ts +5 -0
  82. package/dist/types/deploy/artifacts.d.ts +6 -4
  83. package/dist/types/deploy/headers.d.ts +5 -0
  84. package/dist/types/deploy/platforms/types.d.ts +8 -0
  85. package/dist/types/deploy/redirects.d.ts +24 -13
  86. package/dist/types/markdown/directives.d.ts +62 -0
  87. package/dist/types/markdown/features.d.ts +21 -0
  88. package/dist/types/markdown/mdast.d.ts +63 -0
  89. package/dist/types/openapi/asyncapi.d.ts +4 -2
  90. package/docs/02-deployment.mdx +4 -2
  91. package/docs/08-faq.mdx +1 -1
  92. package/docs/advanced/custom-pages.mdx +3 -3
  93. package/docs/cli/audit.mdx +17 -1
  94. package/docs/cli/evals.mdx +3 -3
  95. package/docs/cli/translate.mdx +3 -3
  96. package/docs/cli/version.mdx +1 -1
  97. package/docs/configuration/analytics.mdx +20 -1
  98. package/docs/configuration/customization.mdx +2 -0
  99. package/docs/configuration/search.mdx +1 -1
  100. package/docs/content/components.mdx +1 -1
  101. package/docs/content/frontmatter.mdx +1 -1
  102. package/docs/content/i18n.mdx +2 -0
  103. package/docs/content/index.mdx +4 -2
  104. package/docs/content/islands.mdx +1 -1
  105. package/docs/content/meta.mdx +3 -1
  106. package/docs/content/sources.mdx +1 -5
  107. package/docs/content/syntax.mdx +14 -0
  108. package/docs/content/versioning.mdx +1 -1
  109. package/docs/discoverability/agent-discovery.mdx +1 -1
  110. package/docs/discoverability/index.mdx +2 -1
  111. package/docs/discoverability/markdown.mdx +2 -2
  112. package/docs/discoverability/open-graph.mdx +5 -3
  113. package/docs/discoverability/rss.mdx +1 -1
  114. package/docs/references/asyncapi.mdx +1 -1
  115. package/docs/references/graphql.mdx +1 -1
  116. package/docs/references/openapi.mdx +5 -3
  117. package/package.json +1 -1
  118. package/skills/blume-migrate/references/fumadocs.md +1 -1
  119. package/skills/blume-migrate/references/nextra.md +1 -1
  120. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
  121. package/src/ai/agent-readability.ts +2 -2
  122. package/src/ai/ai-catalog.ts +6 -2
  123. package/src/ai/api/handlers.ts +2 -2
  124. package/src/ai/api/spec.ts +2 -2
  125. package/src/ai/api-catalog.ts +6 -2
  126. package/src/ai/changelog-markdown.ts +2 -2
  127. package/src/ai/link-headers.ts +14 -5
  128. package/src/ai/llms.ts +18 -7
  129. package/src/ai/mcp/discovery.ts +2 -2
  130. package/src/ai/mcp/query.ts +2 -2
  131. package/src/ai/mcp/server.ts +2 -2
  132. package/src/ai/openapi-components.ts +22 -6
  133. package/src/ai/relative-links.ts +42 -26
  134. package/src/ai/serializers.ts +2 -1
  135. package/src/ai/skills.ts +14 -1
  136. package/src/ai/tar.ts +139 -12
  137. package/src/analytics/head.ts +4 -0
  138. package/src/analytics/index.ts +5 -0
  139. package/src/analytics/one-dollar-stats.ts +86 -0
  140. package/src/analytics/schema.ts +2 -0
  141. package/src/astro/generate.ts +1 -1
  142. package/src/astro/include-hmr.ts +31 -12
  143. package/src/astro/include-refresh.ts +19 -8
  144. package/src/astro/integration.ts +108 -14
  145. package/src/astro/runtime-modules.ts +28 -13
  146. package/src/astro/templates.ts +81 -37
  147. package/src/audit/checks/links.ts +8 -1
  148. package/src/audit/graph.ts +3 -1
  149. package/src/audit/run.ts +12 -9
  150. package/src/audit/snapshot.ts +5 -0
  151. package/src/cli/args.ts +32 -0
  152. package/src/cli/commands/audit.ts +4 -1
  153. package/src/cli/commands/doctor.ts +3 -1
  154. package/src/cli/commands/eval.ts +19 -11
  155. package/src/cli/commands/translate.ts +9 -12
  156. package/src/cli/commands/validate.ts +3 -1
  157. package/src/cli/dev-lock.ts +157 -37
  158. package/src/cli/report-format.ts +11 -6
  159. package/src/components/colors.ts +19 -0
  160. package/src/components/content/Badge.astro +5 -3
  161. package/src/components/content/Component.astro +2 -2
  162. package/src/components/content/Tab.astro +0 -1
  163. package/src/components/content/Tabs.astro +4 -0
  164. package/src/components/content/badge-color.ts +5 -2
  165. package/src/components/content/base-href.ts +13 -36
  166. package/src/components/islands/assistant.tsx +21 -4
  167. package/src/components/islands/base-path.ts +47 -10
  168. package/src/components/islands/hooks.ts +35 -19
  169. package/src/components/islands/webmcp.ts +4 -2
  170. package/src/components/layout/Breadcrumbs.astro +5 -2
  171. package/src/components/layout/DiscoveryLinks.astro +9 -5
  172. package/src/components/layout/Header.astro +2 -2
  173. package/src/components/layout/LanguageSwitcher.astro +2 -2
  174. package/src/components/layout/NavSelector.astro +2 -2
  175. package/src/components/layout/NavTabMenu.astro +3 -3
  176. package/src/components/layout/NavTree.astro +16 -6
  177. package/src/components/layout/PageActions.astro +4 -3
  178. package/src/components/layout/PageLayout.astro +7 -6
  179. package/src/components/layout/Pagination.astro +3 -3
  180. package/src/components/layout/RootLayout.astro +5 -5
  181. package/src/components/layout/Search.astro +33 -13
  182. package/src/components/layout/VersionBanner.astro +2 -2
  183. package/src/components/layout/analytics-client.ts +12 -0
  184. package/src/components/layout/toc-active.ts +41 -0
  185. package/src/components/layout/toc-element.ts +8 -14
  186. package/src/components/openapi/ApiTagOperations.astro +2 -2
  187. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  188. package/src/components/openapi/GraphqlChip.astro +2 -2
  189. package/src/components/openapi/Operation.astro +2 -0
  190. package/src/components/openapi/Playground.astro +4 -4
  191. package/src/components/openapi/RequestPanel.astro +5 -2
  192. package/src/components/openapi/SchemaTable.astro +7 -0
  193. package/src/components/openapi/async.ts +38 -6
  194. package/src/components/openapi/helpers.ts +19 -9
  195. package/src/components/openapi/message-composer.ts +6 -1
  196. package/src/components/openapi/message.ts +17 -2
  197. package/src/components/openapi/operation-model.ts +81 -11
  198. package/src/components/openapi/panel.ts +4 -2
  199. package/src/components/openapi/param-style.ts +181 -0
  200. package/src/components/openapi/playground-client.ts +73 -16
  201. package/src/components/openapi/request.ts +190 -26
  202. package/src/components/openapi/schema-tree.ts +30 -24
  203. package/src/components/openapi/snippets.ts +85 -6
  204. package/src/components/openapi/ws-client.ts +18 -2
  205. package/src/core/base-path.ts +62 -16
  206. package/src/core/config-input.ts +9 -7
  207. package/src/core/diagnostics.ts +2 -0
  208. package/src/core/directive-diagnostics.ts +99 -0
  209. package/src/core/frontmatter.ts +21 -18
  210. package/src/core/heading-markers.ts +5 -18
  211. package/src/core/i18n.ts +31 -3
  212. package/src/core/last-modified.ts +25 -3
  213. package/src/core/locale-links.ts +5 -1
  214. package/src/core/meta.ts +33 -19
  215. package/src/core/navigation.ts +28 -8
  216. package/src/core/project-graph.ts +27 -5
  217. package/src/core/schema.ts +24 -4
  218. package/src/core/sources/contentful-rich-text.ts +27 -20
  219. package/src/core/sources/filesystem.ts +20 -2
  220. package/src/core/sources/github-releases.ts +25 -6
  221. package/src/core/sources/lexical.ts +23 -18
  222. package/src/core/sources/lower.ts +201 -34
  223. package/src/core/sources/mdx-remote.ts +51 -16
  224. package/src/core/sources/normalize.ts +325 -90
  225. package/src/core/sources/notion.ts +52 -25
  226. package/src/core/sources/obsidian.ts +23 -6
  227. package/src/core/sources/portable-text.ts +38 -21
  228. package/src/core/sources/strapi-blocks.ts +20 -16
  229. package/src/core/sources/watch.ts +20 -7
  230. package/src/core/standard-schema.ts +10 -6
  231. package/src/core/version-cut.ts +52 -15
  232. package/src/deploy/artifacts.ts +27 -6
  233. package/src/deploy/cloudflare-negotiation.ts +4 -12
  234. package/src/deploy/headers.ts +8 -4
  235. package/src/deploy/node-headers.ts +1 -1
  236. package/src/deploy/platforms/cloudflare.ts +12 -3
  237. package/src/deploy/platforms/netlify.ts +1 -0
  238. package/src/deploy/platforms/node.ts +1 -0
  239. package/src/deploy/platforms/static.ts +1 -0
  240. package/src/deploy/platforms/types.ts +8 -0
  241. package/src/deploy/platforms/vercel.ts +1 -0
  242. package/src/deploy/redirects.ts +54 -18
  243. package/src/deploy/robots.ts +2 -2
  244. package/src/deploy/rss.ts +4 -3
  245. package/src/deploy/sitemap.ts +7 -5
  246. package/src/markdown/base-links.ts +34 -36
  247. package/src/markdown/directives.ts +242 -36
  248. package/src/markdown/features.ts +17 -0
  249. package/src/markdown/index.ts +7 -6
  250. package/src/markdown/mdast.ts +5 -2
  251. package/src/markdown/relative-links.ts +12 -4
  252. package/src/og/card.ts +129 -5
  253. package/src/og/derive.ts +41 -31
  254. package/src/og/index.ts +1 -0
  255. package/src/openapi/asyncapi.ts +4 -2
  256. package/src/openapi/model.ts +22 -14
  257. package/src/openapi/proxy.ts +63 -10
  258. package/src/openapi/render-mdx.ts +41 -2
  259. package/src/registry/eject.ts +181 -24
  260. package/src/search/adapters/version-scope.ts +30 -0
  261. package/src/search/documents.ts +67 -26
  262. package/src/search/popular.ts +2 -1
  263. package/src/seo/jsonld.ts +7 -3
  264. package/src/translate/meta.ts +68 -24
  265. package/src/translate/run.ts +3 -3
  266. package/src/translate/validate.ts +4 -1
  267. package/src/translate/work-list.ts +56 -17
  268. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  269. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  270. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  271. package/dist/cli/chunk-esh98wmb.js +0 -23
  272. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  273. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  274. package/dist/cli/chunk-j85scx15.js.map +0 -10
  275. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  276. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  277. /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
  278. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
  279. /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
  280. /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
  281. /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
  282. /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
  283. /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
  284. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
  285. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
  286. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
  287. /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
  288. /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
  289. /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
  290. /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
  291. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
  292. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
  293. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
@@ -13,20 +13,50 @@ export const withTrailingSlash = (base: string): string =>
13
13
  export const joinBase = (base: string, path: string): string =>
14
14
  `${withTrailingSlash(base)}${path}`;
15
15
 
16
+ /** A root-relative internal target (`/x`) — never `//host` or a URL. */
17
+ const isRootRelative = (route: string): boolean =>
18
+ route.startsWith("/") && !route.startsWith("//");
19
+
20
+ /** A target's path, without its `?query` or `#fragment`. */
21
+ const pathOf = (route: string): string => {
22
+ const at = route.search(/[#?]/u);
23
+ return at === -1 ? route : route.slice(0, at);
24
+ };
25
+
16
26
  /**
17
- * Prefix a root-relative internal route with the deployment base
18
- * (`/guide` under base `/sub` -> `/sub/guide`), so a rendered link points at the
27
+ * Prefix a root-relative internal link an author wrote with the deployment
28
+ * base (`/guide` under base `/sub` -> `/sub/guide`), so it points at the
19
29
  * page's real served URL. External URLs, protocol-relative URLs, and fragments
20
- * pass through untouched, and it's idempotent (a route already under the base is
21
- * returned unchanged). This is applied only where a URL is *emitted* — the
22
- * navigation model and active-route matching stay in base-less logical space.
30
+ * pass through untouched, and it's idempotent: a link whose path is already
31
+ * under the base (`/sub/guide`, `/sub#install`) is returned unchanged, so a
32
+ * base written by hand isn't doubled. A route Blume generates takes
33
+ * {@link mountBase} instead. This is applied only where a URL is *emitted* —
34
+ * the navigation model and active-route matching stay in base-less logical
35
+ * space.
23
36
  */
24
37
  export const prefixBase = (base: string, route: string): string => {
25
- if (!route.startsWith("/") || route.startsWith("//")) {
38
+ if (!isRootRelative(route)) {
26
39
  return route;
27
40
  }
28
41
  const trimmed = base.replace(/\/+$/u, "");
29
- if (!trimmed || route === trimmed || route.startsWith(`${trimmed}/`)) {
42
+ const path = pathOf(route);
43
+ if (!trimmed || path === trimmed || path.startsWith(`${trimmed}/`)) {
44
+ return route;
45
+ }
46
+ return route === "/" ? trimmed : `${trimmed}${route}`;
47
+ };
48
+
49
+ /**
50
+ * Prefix a route Blume generates (a page, a sidebar entry, a Markdown mirror,
51
+ * an agent artifact) with the deployment base, unconditionally. Unlike
52
+ * {@link prefixBase}, a route whose first segment matches the base is still
53
+ * prefixed: under base `/guides`, the page `guides/setup.md` is served at
54
+ * `/guides/guides/setup`, not at `/guides/setup`. External and
55
+ * protocol-relative URLs pass through untouched.
56
+ */
57
+ export const mountBase = (base: string, route: string): string => {
58
+ const trimmed = base.replace(/\/+$/u, "");
59
+ if (!(trimmed && isRootRelative(route))) {
30
60
  return route;
31
61
  }
32
62
  return route === "/" ? trimmed : `${trimmed}${route}`;
@@ -34,13 +64,20 @@ export const prefixBase = (base: string, route: string): string => {
34
64
 
35
65
  /**
36
66
  * {@link prefixBase} bound to the build-time `BASE_URL` (the resolved
37
- * `deployment.base`). The ergonomic form for `.astro` templates — `href={
38
- * withBase(route)}` — since `BASE_URL` is inlined by Vite wherever this module
39
- * is bundled into the site.
67
+ * `deployment.base`), for links an author wrote. The ergonomic form for
68
+ * `.astro` templates — `href={withBase(href)}` — since `BASE_URL` is inlined
69
+ * by Vite wherever this module is bundled into the site.
40
70
  */
41
71
  export const withBase = (route: string): string =>
42
72
  prefixBase(import.meta.env.BASE_URL ?? "/", route);
43
73
 
74
+ /**
75
+ * {@link mountBase} bound to the build-time `BASE_URL`, for routes Blume
76
+ * generates: `href={withMountedBase(page.route)}`.
77
+ */
78
+ export const withMountedBase = (route: string): string =>
79
+ mountBase(import.meta.env.BASE_URL ?? "/", route);
80
+
44
81
  /**
45
82
  * A pathname with the deployment base stripped (`/docs/guide` -> `/guide`),
46
83
  * for page-context lookups against base-less document routes.
@@ -23,27 +23,29 @@ import { joinBase, stripBase } from "./base-path.ts";
23
23
 
24
24
  export type { BlumeClientData } from "../../core/data.ts";
25
25
 
26
- let cachedData: BlumeClientData | null = null;
26
+ /**
27
+ * The last parsed snapshot, keyed by the script text it came from. Each page
28
+ * renders its own snapshot (its route and title, its locale's navigation), and
29
+ * a client-router navigation swaps in the new page's tag, so the cache is only
30
+ * reused while the tag's text is unchanged.
31
+ */
32
+ let cached: { data: BlumeClientData; text: string } | null = null;
27
33
 
28
- /** Read + parse the injected snapshot (memoized); null on the server. */
34
+ /** Read + parse the current page's injected snapshot (memoized per text). */
29
35
  const readClientData = (): BlumeClientData | null => {
30
- if (cachedData) {
31
- return cachedData;
32
- }
33
- // `document` is undeclared on the server; probing `globalThis` avoids both
34
- // the bare-reference ReferenceError and a `typeof` sniff.
35
- if (!("document" in globalThis)) {
36
+ const text = document.querySelector("#blume-client-data")?.textContent;
37
+ if (!text) {
36
38
  return null;
37
39
  }
38
- const element = document.querySelector("#blume-client-data");
39
- if (!element?.textContent) {
40
- return null;
40
+ if (cached?.text === text) {
41
+ return cached.data;
41
42
  }
42
43
  try {
43
44
  // SAFETY: the layout serialized this script tag's JSON from the same
44
45
  // `BlumeClientData` snapshot this reads back.
45
- cachedData = JSON.parse(element.textContent) as BlumeClientData;
46
- return cachedData;
46
+ const data = JSON.parse(text) as BlumeClientData;
47
+ cached = { data, text };
48
+ return data;
47
49
  } catch {
48
50
  return null;
49
51
  }
@@ -51,15 +53,29 @@ const readClientData = (): BlumeClientData | null => {
51
53
 
52
54
  /**
53
55
  * Read the injected snapshot after mount. `null` on the server and on the first
54
- * client render (so hydration matches), then the data once mounted.
56
+ * client render (so hydration matches), then the data once mounted, re-read
57
+ * after every client-router swap.
55
58
  */
56
59
  const useClientData = (): BlumeClientData | null => {
57
60
  const [data, setData] = useState<BlumeClientData | null>(null);
58
- // Intentional post-mount hydration guard: `null` on the server and first
59
- // client render so hydration matches, then the snapshot once mounted. The
60
- // extra render is required; do not seed the initial value from the DOM.
61
- // oxlint-disable-next-line react/react-compiler, react/set-state-in-effect, react-doctor/no-initialize-state -- deliberate SSR hydration guard
62
- useEffect(() => setData(readClientData()), []);
61
+ useEffect(() => {
62
+ // `document` is undeclared on the server; probing `globalThis` avoids both
63
+ // the bare-reference ReferenceError and a `typeof` sniff.
64
+ if (!("document" in globalThis)) {
65
+ return;
66
+ }
67
+ const sync = () => setData(readClientData());
68
+ // Intentional post-mount hydration guard: `null` on the server and first
69
+ // client render so hydration matches, then the snapshot once mounted. The
70
+ // extra render is required; do not seed the initial value from the DOM.
71
+ // oxlint-disable-next-line react/react-compiler, react/set-state-in-effect, react-doctor/no-initialize-state -- deliberate SSR hydration guard
72
+ sync();
73
+ // An island kept across navigations (`transition:persist`) outlives the
74
+ // page it mounted on; the swap brings the next page's snapshot, so read
75
+ // it again or the hook keeps answering with the first page's data.
76
+ document.addEventListener("astro:after-swap", sync);
77
+ return () => document.removeEventListener("astro:after-swap", sync);
78
+ }, []);
63
79
  return data;
64
80
  };
65
81
 
@@ -1,7 +1,7 @@
1
1
  import { unescape } from "html-escaper";
2
2
 
3
3
  import type { SearchFn } from "../layout/search/types.ts";
4
- import { joinBase, prefixBase } from "./base-path.ts";
4
+ import { joinBase, mountBase, prefixBase } from "./base-path.ts";
5
5
 
6
6
  /**
7
7
  * WebMCP (W3C Web Machine Learning CG): in-page tools an agentic browser can
@@ -144,7 +144,7 @@ export const buildWebMcpTools = (options: WebMcpToolOptions): WebMcpTool[] => {
144
144
  hits
145
145
  .map(
146
146
  (hit) =>
147
- `${plain(hit.title)} — ${prefixBase(options.base, hit.url)}\n${plain(hit.excerpt)}`
147
+ `${plain(hit.title)} — ${mountBase(options.base, hit.url)}\n${plain(hit.excerpt)}`
148
148
  )
149
149
  .join("\n\n")
150
150
  );
@@ -175,6 +175,8 @@ export const buildWebMcpTools = (options: WebMcpToolOptions): WebMcpTool[] => {
175
175
  }
176
176
  const trimmed = route.length > 1 ? route.replace(/\/+$/u, "") : route;
177
177
  const target = trimmed === "/" ? "/index" : trimmed;
178
+ // The agent may hand back a served URL `search_docs` returned, base
179
+ // included, so a path already under the base keeps it (`prefixBase`).
178
180
  const response = await fetchFn(
179
181
  `${prefixBase(options.base, target)}.md`
180
182
  ).catch(() => null);
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import { EN_UI } from "../../core/i18n-ui.ts";
3
3
  import type { UIStrings } from "../../core/i18n-ui.ts";
4
- import { withBase } from "../islands/base-path.ts";
4
+ import { withMountedBase } from "../islands/base-path.ts";
5
5
  import type { Crumb } from "./nav-utils.ts";
6
6
 
7
7
  interface Props {
@@ -35,7 +35,10 @@ const eyebrowCrumb = crumbs.length > 1 ? crumbs[crumbs.length - 2] : null;
35
35
  ]}
36
36
  >
37
37
  {eyebrowCrumb.route ? (
38
- <a class="hover:text-foreground" href={withBase(eyebrowCrumb.route)}>
38
+ <a
39
+ class="hover:text-foreground"
40
+ href={withMountedBase(eyebrowCrumb.route)}
41
+ >
39
42
  {eyebrowCrumb.label}
40
43
  </a>
41
44
  ) : (
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import type { DiscoveryHeadLinks } from "../../core/data.ts";
3
- import { withBase } from "../islands/base-path.ts";
3
+ import { withMountedBase } from "../islands/base-path.ts";
4
4
  // Emits the agent-discovery <link>s for the document <head>, on every page and
5
5
  // not just the root: an agent that enters on a deep page (a search result, a
6
6
  // shared link) never sees the homepage HTTP `Link` header (see
@@ -32,7 +32,7 @@ const { discovery, markdownMirror } = Astro.props;
32
32
  {
33
33
  discovery?.agentReadability && (
34
34
  <link
35
- href={withBase("/agent-readability.json")}
35
+ href={withMountedBase("/agent-readability.json")}
36
36
  rel="describedby"
37
37
  type="application/json"
38
38
  />
@@ -40,7 +40,11 @@ const { discovery, markdownMirror } = Astro.props;
40
40
  }
41
41
  {
42
42
  discovery?.llmsTxt && (
43
- <link href={withBase("/llms.txt")} rel="describedby" type="text/plain" />
43
+ <link
44
+ href={withMountedBase("/llms.txt")}
45
+ rel="describedby"
46
+ type="text/plain"
47
+ />
44
48
  )
45
49
  }
46
50
  {/* The AI Catalog / ARD manifest under both relations its two specs define:
@@ -50,12 +54,12 @@ const { discovery, markdownMirror } = Astro.props;
50
54
  discovery?.aiCatalog && (
51
55
  <>
52
56
  <link
53
- href={withBase("/.well-known/ai-catalog.json")}
57
+ href={withMountedBase("/.well-known/ai-catalog.json")}
54
58
  rel="ai-catalog"
55
59
  type="application/ai-catalog+json"
56
60
  />
57
61
  <link
58
- href={withBase("/.well-known/ard.json")}
62
+ href={withMountedBase("/.well-known/ard.json")}
59
63
  rel="ard"
60
64
  type="application/json"
61
65
  />
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import Ask from "blume:ask";
3
3
  import data from "blume:data";
4
- import { withBase } from "../islands/base-path.ts";
4
+ import { withBase, withMountedBase } from "../islands/base-path.ts";
5
5
  import { NEW_TAB_ATTRS, newTabAttrs } from "../../core/new-tab.ts";
6
6
  import type { ComponentOverride } from "../../core/define-components.ts";
7
7
  import { EN_UI } from "../../core/i18n-ui.ts";
@@ -229,7 +229,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
229
229
  <a
230
230
  aria-current={tab === activeTab ? "page" : undefined}
231
231
  class="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:text-foreground"
232
- href={withBase(tab.href ?? tab.path)}
232
+ href={withMountedBase(tab.href ?? tab.path)}
233
233
  >
234
234
  {tab.icon && (
235
235
  <Icon class="shrink-0" name={tab.icon} size={14} />
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { withBase } from "../islands/base-path.ts";
2
+ import { withMountedBase } from "../islands/base-path.ts";
3
3
  import type { LocaleSwitchOption } from "../../core/types.ts";
4
4
  import Icon from "../Icon.astro";
5
5
 
@@ -42,7 +42,7 @@ const menuRowClass =
42
42
  <a
43
43
  aria-current={option.current ? "true" : undefined}
44
44
  class={menuRowClass}
45
- href={withBase(option.href)}
45
+ href={withMountedBase(option.href)}
46
46
  hreflang={option.code}
47
47
  lang={option.code}
48
48
  >
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { withBase } from "../islands/base-path.ts";
2
+ import { withMountedBase } from "../islands/base-path.ts";
3
3
  // A top-level navigation selector: a dropdown that switches
4
4
  // between partitions of the site — a product, a version, or any grouped set of
5
5
  // destinations (`navigation.selectors` in the config). Built on
@@ -73,7 +73,7 @@ const menuRowClass =
73
73
  <a
74
74
  aria-current={item.path === active?.path ? "true" : undefined}
75
75
  class={menuRowClass}
76
- href={withBase(item.path)}
76
+ href={withMountedBase(item.path)}
77
77
  >
78
78
  {item.icon && <Icon class="mt-0.5" name={item.icon} size={16} />}
79
79
  <span class="flex-1">
@@ -10,7 +10,7 @@
10
10
  // delegate from `document`, so client-router swaps need no re-init.
11
11
  import type { NavTab } from "../../core/types.ts";
12
12
  import Icon from "../Icon.astro";
13
- import { withBase } from "../islands/base-path.ts";
13
+ import { withMountedBase } from "../islands/base-path.ts";
14
14
 
15
15
  interface Props {
16
16
  tab: NavTab;
@@ -60,7 +60,7 @@ const drawerRowClass =
60
60
  <a
61
61
  aria-current={itemCurrent(item.path)}
62
62
  class={barRowClass}
63
- href={withBase(item.path)}
63
+ href={withMountedBase(item.path)}
64
64
  >
65
65
  {item.icon && <Icon class="mt-0.5" name={item.icon} size={16} />}
66
66
  <span class="flex-1">
@@ -102,7 +102,7 @@ const drawerRowClass =
102
102
  <a
103
103
  aria-current={itemCurrent(item.path)}
104
104
  class={drawerRowClass}
105
- href={withBase(item.path)}
105
+ href={withMountedBase(item.path)}
106
106
  >
107
107
  {item.icon && (
108
108
  <Icon class="shrink-0" name={item.icon} size={14} />
@@ -4,7 +4,7 @@ import type { UIStrings } from "../../core/i18n-ui.ts";
4
4
  import type { NavNode } from "../../core/types.ts";
5
5
  import { methodColor } from "../colors.ts";
6
6
  import Icon from "../Icon.astro";
7
- import { withBase } from "../islands/base-path.ts";
7
+ import { withBase, withMountedBase } from "../islands/base-path.ts";
8
8
  import Self from "./NavTree.astro";
9
9
  import NavTreeCache from "./NavTreeCache.astro";
10
10
  import NavTreeScript from "./NavTreeScript.astro";
@@ -82,6 +82,16 @@ const n = { ...EN_UI.nav, ...strings };
82
82
  /** The DOM id of a drill-in panel, which its drill button controls. */
83
83
  const navPanelId = (id: string): string => `blume-nav-panel-${id}`;
84
84
 
85
+ /**
86
+ * A page row's href. A row backed by a page links a route Blume generates, so
87
+ * the deployment base always goes on — even over a route that starts with the
88
+ * base's own name. A config `href` link (no backing page) is authored, and
89
+ * keeps a base written by hand. Group rows link their index page the same way
90
+ * a backed page row does.
91
+ */
92
+ const pageHref = (item: Extract<NavNode, { kind: "page" }>): string =>
93
+ item.pageId ? withMountedBase(item.route) : withBase(item.route);
94
+
85
95
  const badgeBase =
86
96
  "shrink-0 rounded-full px-1.5 py-0.5 font-medium text-[0.65rem] leading-none";
87
97
  // HTTP-method, AsyncAPI-action, and GraphQL operation-kind badges (from a
@@ -191,7 +201,7 @@ const initialId =
191
201
  : undefined
192
202
  }
193
203
  class="flex-1 truncate rounded-[0.65rem] px-1 py-1 font-semibold text-foreground text-sm transition-colors hover:bg-muted"
194
- href={withBase(panel.route)}
204
+ href={withMountedBase(panel.route)}
195
205
  >
196
206
  {panel.label}
197
207
  </a>
@@ -257,7 +267,7 @@ const initialId =
257
267
  <a
258
268
  aria-current={current}
259
269
  class="blume-nav-link truncate"
260
- href={withBase(item.route)}
270
+ href={pageHref(item)}
261
271
  >
262
272
  {item.label}
263
273
  </a>
@@ -269,7 +279,7 @@ const initialId =
269
279
  <a
270
280
  aria-current={current}
271
281
  class="blume-nav-link"
272
- href={withBase(item.route)}
282
+ href={pageHref(item)}
273
283
  >
274
284
  <span class="flex items-center gap-2">
275
285
  {item.icon && (
@@ -348,7 +358,7 @@ const initialId =
348
358
  : undefined
349
359
  }
350
360
  class="blume-nav-summary-link"
351
- href={withBase(item.route)}
361
+ href={withMountedBase(item.route)}
352
362
  >
353
363
  {item.icon && (
354
364
  <Icon
@@ -432,7 +442,7 @@ const initialId =
432
442
  isGroupRowCurrent(item, currentRoute) ? "page" : undefined
433
443
  }
434
444
  class="blume-nav-heading-link"
435
- href={withBase(item.route)}
445
+ href={withMountedBase(item.route)}
436
446
  >
437
447
  {item.icon && (
438
448
  <Icon
@@ -262,7 +262,7 @@ const menuRowClass =
262
262
  <script>
263
263
  import { copyDeferredText, copyText, flashLabel } from "../copy-feedback.ts";
264
264
  import { installDropdownDismiss } from "../dropdown-dismiss.ts";
265
- import { prefixBase } from "../islands/base-path.ts";
265
+ import { mountBase } from "../islands/base-path.ts";
266
266
  import { rafThrottle } from "../raf-throttle.ts";
267
267
 
268
268
  const CHAT_URLS: Record<string, (q: string) => string> = {
@@ -390,9 +390,10 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
390
390
 
391
391
  // `data-md` is the base-less logical route; the raw-markdown endpoint is a
392
392
  // generated page route, so it's served under the deployment base like any
393
- // other page. Prefix at emit time (the Search-island pattern).
393
+ // other page, even when it starts with the base's own name. Mount it at
394
+ // emit time (the Search-island pattern).
394
395
  const mdRoute = root.getAttribute("data-md") ?? "";
395
- const md = prefixBase(import.meta.env.BASE_URL, mdRoute);
396
+ const md = mountBase(import.meta.env.BASE_URL, mdRoute);
396
397
  const copiedLabel = root.getAttribute("data-i18n-copied") || "Copied!";
397
398
  const copyFailedLabel =
398
399
  root.getAttribute("data-i18n-copy-failed") || "Copy failed";
@@ -39,7 +39,7 @@ import {
39
39
  import { buildStructuredData } from "../../seo/jsonld.ts";
40
40
  import { normalizeXHandle } from "../../seo/x-handle.ts";
41
41
  import Icon from "../Icon.astro";
42
- import { stripBase, withBase } from "../islands/base-path.ts";
42
+ import { stripBase, withBase, withMountedBase } from "../islands/base-path.ts";
43
43
  import { pageDirection, pageLocale } from "./page-locale.ts";
44
44
  import { searchLocaleFor } from "./search-locale.ts";
45
45
  import "blume:theme";
@@ -219,7 +219,7 @@ const activeTab = currentTabForRoute(navigation.tabs, route, navigation.root);
219
219
  // llms.txt index), and the homepage HTTP `Link` header already advertises it,
220
220
  // so the head is its HTML counterpart on hosts that can't set that header.
221
221
  // Advertising a mirror for any other custom route would 404.
222
- const markdownMirror = route === "/" ? withBase("/index.md") : null;
222
+ const markdownMirror = route === "/" ? withMountedBase("/index.md") : null;
223
223
 
224
224
  // Derive canonical + og:image from the site URL the same way the catch-all does
225
225
  // for content pages, so a custom page gets both for free. The matching OG card
@@ -230,8 +230,9 @@ const markdownMirror = route === "/" ? withBase("/index.md") : null;
230
230
  const ogSlug = route === "/" ? "index" : route.slice(1);
231
231
  const ogCardGenerated = !Astro.routePattern.includes("[");
232
232
  // Absolute URLs carry the deployment base too (`site + base + route`), since the
233
- // page is served under it. `withBase` handles a root base as a no-op.
234
- const basedRoute = withBase(route);
233
+ // page is served under it — even when the route starts with the base's own
234
+ // name. `withMountedBase` handles a root base as a no-op.
235
+ const basedRoute = withMountedBase(route);
235
236
  // The schema accepts a trailing-slash site URL (`https://docs.example.com/`);
236
237
  // strip it before joining with the root-relative route so canonical/og URLs
237
238
  // don't come out double-slashed — the catch-all strips it the same way.
@@ -253,7 +254,7 @@ const absolutizeOgImage = (value: string): string =>
253
254
  const resolvedOgImage = ogImage
254
255
  ? absolutizeOgImage(ogImage)
255
256
  : ogEnabled && ogCardGenerated && siteBase
256
- ? `${siteBase}${encodeURI(withBase(`/og/${ogSlug}.png`))}`
257
+ ? `${siteBase}${encodeURI(withMountedBase(`/og/${ogSlug}.png`))}`
257
258
  : null;
258
259
  // Only the generated card has a known size and format, so its dimensions are
259
260
  // declared for crawlers; a user-supplied `ogImage` could be any image.
@@ -439,7 +440,7 @@ createIconSprite(Astro.locals);
439
440
  <a
440
441
  aria-current={tab === activeTab ? "page" : undefined}
441
442
  class="flex items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
442
- href={withBase(tab.href ?? tab.path)}
443
+ href={withMountedBase(tab.href ?? tab.path)}
443
444
  >
444
445
  {tab.icon && (
445
446
  <Icon class="shrink-0" name={tab.icon} size={14} />
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { withBase } from "../islands/base-path.ts";
2
+ import { withMountedBase } from "../islands/base-path.ts";
3
3
  import { EN_UI } from "../../core/i18n-ui.ts";
4
4
  import type { UIStrings } from "../../core/i18n-ui.ts";
5
5
  import type { FlatPage } from "./nav-utils.ts";
@@ -31,7 +31,7 @@ const s = { ...EN_UI.page, ...strings };
31
31
  {prev ? (
32
32
  <a
33
33
  class="flex max-w-[48%] flex-1 items-center gap-2 rounded-blume border border-border px-4 py-3 text-foreground transition-colors hover:border-foreground"
34
- href={withBase(prev.route)}
34
+ href={withMountedBase(prev.route)}
35
35
  >
36
36
  <Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
37
37
  <span class="min-w-0">
@@ -47,7 +47,7 @@ const s = { ...EN_UI.page, ...strings };
47
47
  {next && (
48
48
  <a
49
49
  class="ms-auto flex max-w-[48%] flex-1 items-center justify-end gap-2 rounded-blume border border-border px-4 py-3 text-end text-foreground transition-colors hover:border-foreground"
50
- href={withBase(next.route)}
50
+ href={withMountedBase(next.route)}
51
51
  >
52
52
  <span class="min-w-0">
53
53
  <span class="block text-muted-foreground text-xs">
@@ -39,7 +39,7 @@ import {
39
39
  } from "../../og/dimensions.ts";
40
40
  import { buildStructuredData, toIso } from "../../seo/jsonld.ts";
41
41
  import { normalizeXHandle } from "../../seo/x-handle.ts";
42
- import { withBase } from "../islands/base-path.ts";
42
+ import { withBase, withMountedBase } from "../islands/base-path.ts";
43
43
  import { isExternalUrl } from "../../core/base-path.ts";
44
44
  import { NEW_TAB_HINT_ID, newTabAttrs } from "../../core/new-tab.ts";
45
45
  import Analytics from "./Analytics.astro";
@@ -401,12 +401,12 @@ const formattedLastModified =
401
401
  // not a content route and has none.
402
402
  const markdownMirror = isBare
403
403
  ? null
404
- : withBase(page.route === "/" ? "/index.md" : `${page.route}.md`);
404
+ : withMountedBase(page.route === "/" ? "/index.md" : `${page.route}.md`);
405
405
 
406
406
  // The hosted MCP server's absolute URL, used by the page-actions install menu.
407
407
  // Needs a configured site to be useful, so the menu is hidden without one.
408
408
  const mcpUrl =
409
- mcp && siteUrl ? new URL(withBase(mcp.route), siteUrl).href : null;
409
+ mcp && siteUrl ? new URL(withMountedBase(mcp.route), siteUrl).href : null;
410
410
 
411
411
  // Scope the sidebar (and the breadcrumbs/pagination derived from it) to the
412
412
  // active tab's section, so a multi-section site drills each tab into its own
@@ -540,7 +540,7 @@ createIconSprite(Astro.locals);
540
540
  {
541
541
  feeds?.map((feed) => (
542
542
  <link
543
- href={withBase(feed.href)}
543
+ href={withMountedBase(feed.href)}
544
544
  rel="alternate"
545
545
  title={feed.title}
546
546
  type="application/rss+xml"
@@ -689,7 +689,7 @@ createIconSprite(Astro.locals);
689
689
  <a
690
690
  aria-current={tab === activeTab ? "page" : undefined}
691
691
  class="flex items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
692
- href={withBase(tab.href ?? tab.path)}
692
+ href={withMountedBase(tab.href ?? tab.path)}
693
693
  >
694
694
  {tab.icon && (
695
695
  <Icon class="shrink-0" name={tab.icon} size={14} />