boltdocs 2.6.2 → 2.7.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 (177) hide show
  1. package/bin/boltdocs.js +0 -1
  2. package/dist/cache-CQKlT4fI.mjs +6 -0
  3. package/dist/cache-DorPMFgW.cjs +6 -0
  4. package/dist/cards-BLoSiRuL.d.ts +30 -0
  5. package/dist/cards-CQn9mXZS.d.cts +30 -0
  6. package/dist/chunk-Ds5LZdWN.cjs +6 -0
  7. package/dist/client/index.cjs +1 -1
  8. package/dist/client/index.d.cts +167 -1338
  9. package/dist/client/index.d.ts +166 -1337
  10. package/dist/client/index.js +1 -1
  11. package/dist/{package-CFP44vfn.cjs → client/mdx.cjs} +1 -1
  12. package/dist/client/mdx.d.cts +128 -0
  13. package/dist/client/mdx.d.ts +129 -0
  14. package/dist/client/mdx.js +6 -0
  15. package/dist/client/primitives.cjs +6 -0
  16. package/dist/client/primitives.d.cts +818 -0
  17. package/dist/client/primitives.d.ts +818 -0
  18. package/dist/client/primitives.js +6 -0
  19. package/dist/client/theme/neutral.css +74 -361
  20. package/dist/client/theme/reset.css +189 -0
  21. package/dist/docs-layout-BlDhcQRv.cjs +6 -0
  22. package/dist/docs-layout-BvAOWEJw.js +6 -0
  23. package/dist/doctor-BQiQhCTl.cjs +6 -0
  24. package/dist/doctor-COpf35L2.cjs +20 -0
  25. package/dist/doctor-Dh1XP7Pz.mjs +20 -0
  26. package/dist/generator-DGW6pkCC.cjs +22 -0
  27. package/dist/generator-Dv3wEmhZ.mjs +22 -0
  28. package/dist/icons-dev-CrQLjoQp.js +6 -0
  29. package/dist/icons-dev-rzdz6Lf3.cjs +6 -0
  30. package/dist/image-BkIfa9oo.js +6 -0
  31. package/dist/image-DIGjCPe6.cjs +6 -0
  32. package/dist/mdx-K0WYBAJ3.js +7 -0
  33. package/dist/mdx-hpErbRUe.cjs +7 -0
  34. package/dist/meta-loader-0gJ4PtBC.cjs +6 -0
  35. package/dist/meta-loader-9IpAHWDS.mjs +6 -0
  36. package/dist/node/cli-entry.cjs +1 -2
  37. package/dist/node/cli-entry.mjs +1 -2
  38. package/dist/node/index.cjs +1 -1
  39. package/dist/node/index.d.cts +55 -11
  40. package/dist/node/index.d.mts +55 -12
  41. package/dist/node/index.mjs +1 -1
  42. package/dist/node/routes/worker.cjs +6 -0
  43. package/dist/node/routes/worker.d.cts +2 -0
  44. package/dist/node/routes/worker.d.mts +2 -0
  45. package/dist/node/routes/worker.mjs +6 -0
  46. package/dist/node-C2nWXElP.mjs +112 -0
  47. package/dist/node-CinkUtxV.cjs +112 -0
  48. package/dist/package-BMYLDBBP.cjs +6 -0
  49. package/dist/{package-Bqbn1AYK.mjs → package-HegMOTL_.mjs} +1 -1
  50. package/dist/parser-Bh11BsdA.cjs +6 -0
  51. package/dist/parser-D8eQvE7N.mjs +6 -0
  52. package/dist/parser-DYRzXWmA.cjs +6 -0
  53. package/dist/routes-CHf76Ye4.cjs +6 -0
  54. package/dist/routes-CMUZGI6T.mjs +6 -0
  55. package/dist/routes-Co1mRM58.cjs +6 -0
  56. package/dist/search-dialog-BACuzoVX.cjs +6 -0
  57. package/dist/search-dialog-BKagVT17.js +6 -0
  58. package/dist/search-dialog-C8w12eUx.js +6 -0
  59. package/dist/search-dialog-CGyrozZE.cjs +6 -0
  60. package/dist/search-dialog-D26rUnJ_.cjs +6 -0
  61. package/dist/sidebar-DKvg6KOc.d.cts +491 -0
  62. package/dist/sidebar-Dr1TiRIy.d.ts +491 -0
  63. package/dist/utils-BxNAXhZZ.mjs +7 -0
  64. package/dist/utils-Clzu7jvb.cjs +7 -0
  65. package/dist/worker-pool-Bd8Y9KDv.mjs +6 -0
  66. package/dist/worker-pool-BwU8ckrg.cjs +6 -0
  67. package/package.json +27 -8
  68. package/src/client/app/doc-page.tsx +9 -5
  69. package/src/client/app/docs-layout.tsx +17 -3
  70. package/src/client/app/head.tsx +122 -0
  71. package/src/client/app/helmet-compat.tsx +36 -0
  72. package/src/client/app/mdx-component.tsx +5 -52
  73. package/src/client/app/mdx-components-context.tsx +32 -8
  74. package/src/client/app/routes-context.tsx +2 -2
  75. package/src/client/app/scroll-handler.tsx +1 -1
  76. package/src/client/app/theme-context.tsx +5 -5
  77. package/src/client/app/ui-context.tsx +42 -0
  78. package/src/client/components/docs-layout-default.tsx +85 -0
  79. package/src/client/components/icons-dev.tsx +38 -15
  80. package/src/client/components/mdx/callout.tsx +97 -0
  81. package/src/client/components/mdx/card.tsx +73 -98
  82. package/src/client/components/mdx/cards.tsx +27 -0
  83. package/src/client/components/mdx/code-block.tsx +37 -17
  84. package/src/client/components/mdx/field.tsx +24 -56
  85. package/src/client/components/mdx/image.tsx +36 -15
  86. package/src/client/components/mdx/index.ts +19 -53
  87. package/src/client/components/mdx/table.tsx +46 -148
  88. package/src/client/components/mdx/typographics.tsx +120 -0
  89. package/src/client/components/mdx/{hooks/use-code-block.ts → use-code-block.ts} +5 -7
  90. package/src/client/components/primitives/breadcrumbs.tsx +5 -24
  91. package/src/client/components/primitives/button.tsx +3 -142
  92. package/src/client/components/primitives/code-block.tsx +104 -97
  93. package/src/client/components/{docs-layout.tsx → primitives/docs-layout.tsx} +15 -24
  94. package/src/client/components/primitives/error-boundary.tsx +107 -0
  95. package/src/client/components/primitives/heading.tsx +128 -0
  96. package/src/client/components/primitives/helpers/observer.ts +62 -32
  97. package/src/client/components/primitives/image.tsx +26 -0
  98. package/src/client/components/primitives/link.tsx +50 -52
  99. package/src/client/components/primitives/menu.tsx +25 -49
  100. package/src/client/components/primitives/navbar.tsx +234 -59
  101. package/src/client/components/primitives/on-this-page.tsx +169 -40
  102. package/src/client/components/primitives/page-nav.tsx +11 -39
  103. package/src/client/components/primitives/popover.tsx +12 -30
  104. package/src/client/components/primitives/search-dialog.tsx +77 -71
  105. package/src/client/components/primitives/sidebar.tsx +312 -119
  106. package/src/client/components/primitives/skeleton.tsx +1 -1
  107. package/src/client/components/primitives/tabs.tsx +5 -16
  108. package/src/client/components/primitives/tooltip.tsx +1 -1
  109. package/src/client/components/ui-base/banner.tsx +66 -0
  110. package/src/client/components/ui-base/breadcrumbs.tsx +26 -20
  111. package/src/client/components/ui-base/copy-markdown.tsx +43 -35
  112. package/src/client/components/ui-base/error-boundary.tsx +9 -46
  113. package/src/client/components/ui-base/github-stars.tsx +5 -3
  114. package/src/client/components/ui-base/index.ts +3 -3
  115. package/src/client/components/ui-base/last-updated.tsx +27 -0
  116. package/src/client/components/ui-base/navbar.tsx +183 -89
  117. package/src/client/components/ui-base/not-found.tsx +11 -9
  118. package/src/client/components/ui-base/on-this-page.tsx +8 -104
  119. package/src/client/components/ui-base/page-nav.tsx +23 -9
  120. package/src/client/components/ui-base/search-dialog.tsx +111 -36
  121. package/src/client/components/ui-base/search-highlight.tsx +10 -0
  122. package/src/client/components/ui-base/sidebar.tsx +77 -154
  123. package/src/client/components/ui-base/tabs.tsx +20 -7
  124. package/src/client/components/ui-base/theme-toggle.tsx +88 -10
  125. package/src/client/components/ui-base/version-i18n.tsx +80 -0
  126. package/src/client/hooks/index.ts +2 -1
  127. package/src/client/hooks/use-analytics.ts +272 -0
  128. package/src/client/hooks/use-i18n.ts +116 -50
  129. package/src/client/hooks/use-localized-to.ts +70 -27
  130. package/src/client/hooks/use-navbar.ts +69 -39
  131. package/src/client/hooks/use-page-nav.ts +28 -25
  132. package/src/client/hooks/use-routes.ts +63 -80
  133. package/src/client/hooks/use-search-highlight.ts +185 -0
  134. package/src/client/hooks/use-search.ts +12 -3
  135. package/src/client/hooks/use-sidebar.ts +183 -80
  136. package/src/client/hooks/use-tabs.ts +3 -4
  137. package/src/client/hooks/use-version.ts +44 -29
  138. package/src/client/index.ts +13 -87
  139. package/src/client/mdx.ts +2 -0
  140. package/src/client/primitives.ts +19 -0
  141. package/src/client/ssg/boltdocs-shell.tsx +68 -79
  142. package/src/client/ssg/create-routes.tsx +268 -72
  143. package/src/client/ssg/mdx-page.tsx +2 -1
  144. package/src/client/store/boltdocs-context.tsx +72 -20
  145. package/src/client/theme/neutral.css +74 -361
  146. package/src/client/theme/reset.css +189 -0
  147. package/src/client/types.ts +10 -2
  148. package/src/client/utils/path.ts +9 -0
  149. package/src/client/utils/react-to-text.ts +24 -24
  150. package/src/client/virtual.d.ts +1 -1
  151. package/src/shared/types.ts +82 -22
  152. package/dist/node-Bogvkxao.mjs +0 -101
  153. package/dist/node-CXaog6St.cjs +0 -101
  154. package/dist/search-dialog-CV3eJzMm.cjs +0 -6
  155. package/dist/search-dialog-DNTomKgu.js +0 -6
  156. package/dist/use-search-CS3gH19M.js +0 -6
  157. package/dist/use-search-DBpJZQuw.cjs +0 -6
  158. package/src/client/components/mdx/admonition.tsx +0 -91
  159. package/src/client/components/mdx/badge.tsx +0 -41
  160. package/src/client/components/mdx/button.tsx +0 -35
  161. package/src/client/components/mdx/component-preview.tsx +0 -37
  162. package/src/client/components/mdx/component-props.tsx +0 -83
  163. package/src/client/components/mdx/file-tree.tsx +0 -325
  164. package/src/client/components/mdx/hooks/use-component-preview.ts +0 -16
  165. package/src/client/components/mdx/hooks/useTable.ts +0 -74
  166. package/src/client/components/mdx/hooks/useTabs.ts +0 -68
  167. package/src/client/components/mdx/link.tsx +0 -38
  168. package/src/client/components/mdx/list.tsx +0 -192
  169. package/src/client/components/mdx/tabs.tsx +0 -135
  170. package/src/client/components/mdx/video.tsx +0 -68
  171. package/src/client/components/primitives/index.ts +0 -19
  172. package/src/client/components/primitives/navigation-menu.tsx +0 -114
  173. package/src/client/components/ui-base/head.tsx +0 -83
  174. package/src/client/components/ui-base/loading.tsx +0 -57
  175. package/src/client/components/ui-base/powered-by.tsx +0 -25
  176. package/src/client/hooks/use-onthispage.ts +0 -23
  177. package/src/client/utils/use-on-change.ts +0 -15
@@ -0,0 +1,272 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react'
2
+ import { useLocation } from './use-location'
3
+ import type { BoltdocsIntegrationsConfig } from '../../shared/types'
4
+
5
+ declare global {
6
+ interface Window {
7
+ gtag?: (...args: unknown[]) => void
8
+ dataLayer?: unknown[]
9
+ gtag_report_conversion?: (url?: string) => boolean
10
+ }
11
+ }
12
+
13
+ export interface AnalyticsEvent {
14
+ action: string
15
+ category?: string
16
+ label?: string
17
+ value?: number
18
+ params?: Record<string, unknown>
19
+ }
20
+
21
+ export interface AnalyticsInstance {
22
+ trackPageView: (path: string, title?: string) => void
23
+ trackEvent: (event: AnalyticsEvent) => void
24
+ trackSearch: (query: string, resultsCount?: number) => void
25
+ trackDownload: (file: string, type?: string) => void
26
+ trackExternalLink: (url: string) => void
27
+ isEnabled: boolean
28
+ }
29
+
30
+ function createAnalyticsInstance(
31
+ config?: BoltdocsIntegrationsConfig,
32
+ ): AnalyticsInstance {
33
+ if (typeof window === 'undefined') {
34
+ return createDisabledAnalytics()
35
+ }
36
+
37
+ const isGtagAvailable = typeof window.gtag === 'function'
38
+
39
+ if (isGtagAvailable) {
40
+ return createGtagAnalytics(config)
41
+ }
42
+
43
+ if (window.dataLayer) {
44
+ return createDataLayerAnalytics(config)
45
+ }
46
+
47
+ return createDisabledAnalytics()
48
+ }
49
+
50
+ function createGtagAnalytics(
51
+ config?: BoltdocsIntegrationsConfig,
52
+ ): AnalyticsInstance {
53
+ return {
54
+ trackPageView: (path: string, title?: string) => {
55
+ window.gtag?.('event', 'page_view', {
56
+ page_path: path,
57
+ page_title: title || document.title,
58
+ send_to: config?.ga4?.measurementId,
59
+ })
60
+ },
61
+ trackEvent: ({ action, category, label, value, params }) => {
62
+ window.gtag?.('event', action, {
63
+ event_category: category,
64
+ event_label: label,
65
+ value,
66
+ send_to: config?.ga4?.measurementId,
67
+ ...params,
68
+ })
69
+ },
70
+ trackSearch: (query: string, resultsCount?: number) => {
71
+ window.gtag?.('event', 'search', {
72
+ search_term: query,
73
+ results_count: resultsCount,
74
+ send_to: config?.ga4?.measurementId,
75
+ })
76
+ },
77
+ trackDownload: (file: string, type?: string) => {
78
+ window.gtag?.('event', 'file_download', {
79
+ file_name: file,
80
+ file_type: type || file.split('.').pop(),
81
+ send_to: config?.ga4?.measurementId,
82
+ })
83
+ },
84
+ trackExternalLink: (url: string) => {
85
+ window.gtag?.('event', 'external_link', {
86
+ link_url: url,
87
+ send_to: config?.ga4?.measurementId,
88
+ })
89
+ },
90
+ isEnabled: true,
91
+ }
92
+ }
93
+
94
+ function createDataLayerAnalytics(
95
+ config?: BoltdocsIntegrationsConfig,
96
+ ): AnalyticsInstance {
97
+ return {
98
+ trackPageView: (path: string, title?: string) => {
99
+ window.dataLayer?.push({
100
+ event: 'page_view',
101
+ page_path: path,
102
+ page_title: title || document.title,
103
+ send_to: config?.gtm?.tagId,
104
+ })
105
+ },
106
+ trackEvent: ({ action, category, label, value, params }) => {
107
+ window.dataLayer?.push({
108
+ event: action,
109
+ event_category: category,
110
+ event_label: label,
111
+ value,
112
+ send_to: config?.gtm?.tagId,
113
+ ...params,
114
+ })
115
+ },
116
+ trackSearch: (query: string, resultsCount?: number) => {
117
+ window.dataLayer?.push({
118
+ event: 'search',
119
+ search_term: query,
120
+ results_count: resultsCount,
121
+ send_to: config?.gtm?.tagId,
122
+ })
123
+ },
124
+ trackDownload: (file: string, type?: string) => {
125
+ window.dataLayer?.push({
126
+ event: 'file_download',
127
+ file_name: file,
128
+ file_type: type || file.split('.').pop(),
129
+ send_to: config?.gtm?.tagId,
130
+ })
131
+ },
132
+ trackExternalLink: (url: string) => {
133
+ window.dataLayer?.push({
134
+ event: 'external_link',
135
+ link_url: url,
136
+ send_to: config?.gtm?.tagId,
137
+ })
138
+ },
139
+ isEnabled: true,
140
+ }
141
+ }
142
+
143
+ function createDisabledAnalytics(): AnalyticsInstance {
144
+ return {
145
+ trackPageView: () => {},
146
+ trackEvent: () => {},
147
+ trackSearch: () => {},
148
+ trackDownload: () => {},
149
+ trackExternalLink: () => {},
150
+ isEnabled: false,
151
+ }
152
+ }
153
+
154
+ export interface UseAnalyticsOptions {
155
+ config?: BoltdocsIntegrationsConfig
156
+ autoTrackPageViews?: boolean
157
+ autoTrackDownloads?: boolean
158
+ autoTrackExternalLinks?: boolean
159
+ excludePatterns?: RegExp[]
160
+ }
161
+
162
+ const CONFIG_INSTANCE_SYMBOL = Symbol.for('__BDOCS_CONFIG_INSTANCE__')
163
+
164
+ export function useAnalytics(options: UseAnalyticsOptions = {}) {
165
+ const {
166
+ config: optionsConfig,
167
+ autoTrackPageViews = true,
168
+ autoTrackDownloads = true,
169
+ autoTrackExternalLinks = true,
170
+ excludePatterns = [],
171
+ } = options
172
+
173
+ const globalConfig =
174
+ typeof globalThis !== 'undefined'
175
+ ? ((globalThis as any)[CONFIG_INSTANCE_SYMBOL] as
176
+ | { integrations?: BoltdocsIntegrationsConfig }
177
+ | undefined)
178
+ : undefined
179
+ const config = optionsConfig ?? globalConfig?.integrations
180
+
181
+ const analytics = useMemo(() => createAnalyticsInstance(config), [config])
182
+
183
+ const previousPath = useRef<string>('')
184
+ const location = useLocation()
185
+
186
+ useEffect(() => {
187
+ if (!autoTrackPageViews || !analytics.isEnabled) return
188
+
189
+ const path = location.pathname + location.search
190
+
191
+ if (path !== previousPath.current) {
192
+ previousPath.current = path
193
+ analytics.trackPageView(path, document.title)
194
+ }
195
+ }, [location.pathname, autoTrackPageViews, analytics])
196
+
197
+ useEffect(() => {
198
+ if (!autoTrackDownloads || !analytics.isEnabled) return
199
+
200
+ const handleClick = (event: MouseEvent) => {
201
+ const target = (event.target as Element)?.closest('a')
202
+ if (!target) return
203
+
204
+ const href = target.getAttribute('href')
205
+ if (!href) return
206
+
207
+ if (excludePatterns.some((pattern) => pattern.test(href))) return
208
+
209
+ const isDownload =
210
+ target.hasAttribute('download') ||
211
+ /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|zip|rar|7z|tar|gz|mp3|mp4|avi|mov|png|jpg|jpeg|gif|svg|webp)$/i.test(
212
+ href,
213
+ )
214
+
215
+ if (isDownload) {
216
+ const fileName = href.split('/').pop() || href
217
+ analytics.trackDownload(fileName, fileName.split('.').pop())
218
+ }
219
+ }
220
+
221
+ document.addEventListener('click', handleClick)
222
+ return () => document.removeEventListener('click', handleClick)
223
+ }, [autoTrackDownloads, autoTrackExternalLinks, analytics, excludePatterns])
224
+
225
+ useEffect(() => {
226
+ if (!autoTrackExternalLinks || !analytics.isEnabled) return
227
+
228
+ const handleClick = (event: MouseEvent) => {
229
+ const target = (event.target as Element)?.closest('a')
230
+ if (!target) return
231
+
232
+ const href = target.getAttribute('href')
233
+ if (!href) return
234
+
235
+ if (excludePatterns.some((pattern) => pattern.test(href))) return
236
+
237
+ const isExternal =
238
+ href.startsWith('http://') ||
239
+ href.startsWith('https://') ||
240
+ href.startsWith('//')
241
+
242
+ if (isExternal && !href.includes(window.location.hostname)) {
243
+ analytics.trackExternalLink(href)
244
+ }
245
+ }
246
+
247
+ document.addEventListener('click', handleClick)
248
+ return () => document.removeEventListener('click', handleClick)
249
+ }, [autoTrackExternalLinks, analytics, excludePatterns])
250
+
251
+ return analytics
252
+ }
253
+
254
+ export function useTrackPageView() {
255
+ const analytics = useMemo(() => createAnalyticsInstance(), [])
256
+ return useCallback(
257
+ (path: string, title?: string) => {
258
+ analytics.trackPageView(path, title)
259
+ },
260
+ [analytics],
261
+ )
262
+ }
263
+
264
+ export function useTrackEvent() {
265
+ const analytics = useMemo(() => createAnalyticsInstance(), [])
266
+ return useCallback(
267
+ (event: AnalyticsEvent) => {
268
+ analytics.trackEvent(event)
269
+ },
270
+ [analytics],
271
+ )
272
+ }
@@ -1,6 +1,8 @@
1
+ import { useMemo } from 'react'
1
2
  import { useNavigate } from 'react-router-dom'
2
3
  import { getBaseFilePath } from '../utils/get-base-file-path'
3
4
  import { useRoutes } from './use-routes'
5
+ import { useConfig } from '../app/config-context'
4
6
  import { useBoltdocsContext } from '../store/boltdocs-context'
5
7
  import type { BoltdocsLocale } from '../../shared/types'
6
8
 
@@ -23,8 +25,8 @@ export interface UseI18nReturn {
23
25
  */
24
26
  export function useI18n(): UseI18nReturn {
25
27
  const navigate = useNavigate()
26
- const routeContext = useRoutes()
27
- const { allRoutes, currentRoute, currentLocale, config } = routeContext
28
+ const config = useConfig()
29
+ const { allRoutes, currentRoute, currentLocale, currentVersion } = useRoutes()
28
30
  const i18n = config.i18n
29
31
  const { setLocale } = useBoltdocsContext()
30
32
 
@@ -34,78 +36,142 @@ export function useI18n(): UseI18nReturn {
34
36
  // Update store
35
37
  setLocale(locale)
36
38
 
37
- let targetPath = '/'
39
+ const base = config.base || '/'
40
+ const safeBase = base === '/' ? '' : base.replace(/\/$/, '')
41
+ const isDocRoute = !!currentRoute?.filePath
42
+
43
+ let targetPath = ''
38
44
 
39
45
  if (currentRoute) {
40
- const baseFile = getBaseFilePath(
41
- currentRoute.filePath,
42
- currentRoute.version,
43
- currentRoute.locale,
44
- )
45
- const targetRoute = allRoutes.find(
46
- (r) =>
47
- getBaseFilePath(r.filePath, r.version, r.locale) === baseFile &&
48
- (r.locale || i18n.defaultLocale) === locale &&
49
- r.version === currentRoute.version,
50
- )
46
+ // Case A: We are on a known route. Determine if it is doc or external.
47
+ if (isDocRoute) {
48
+ // Documentation Context logic
49
+ const baseFile = getBaseFilePath(
50
+ currentRoute.filePath,
51
+ currentRoute.version,
52
+ currentRoute.locale,
53
+ )
51
54
 
52
- if (targetRoute) {
53
- targetPath = targetRoute.path
54
- } else {
55
- const defaultIndexRoute = allRoutes.find(
55
+ const targetRoute = allRoutes.find(
56
56
  (r) =>
57
- getBaseFilePath(r.filePath, r.version, r.locale) === 'index.md' &&
57
+ getBaseFilePath(r.filePath, r.version, r.locale) === baseFile &&
58
58
  (r.locale || i18n.defaultLocale) === locale &&
59
59
  r.version === currentRoute.version,
60
60
  )
61
- targetPath = defaultIndexRoute
62
- ? defaultIndexRoute.path
63
- : locale === i18n.defaultLocale
64
- ? currentRoute.version
65
- ? `/${currentRoute.version}`
66
- : '/'
67
- : currentRoute.version
68
- ? `/${currentRoute.version}/${locale}`
69
- : `/${locale}`
61
+
62
+ if (targetRoute) {
63
+ targetPath = targetRoute.path
64
+ } else {
65
+ // Recovery: Find target index, or hardcode reconstruct using version space
66
+ const defaultIndexRoute = allRoutes.find(
67
+ (r) =>
68
+ getBaseFilePath(r.filePath, r.version, r.locale) === 'index.md' &&
69
+ (r.locale || i18n.defaultLocale) === locale &&
70
+ r.version === currentRoute.version,
71
+ )
72
+
73
+ if (defaultIndexRoute) {
74
+ targetPath = defaultIndexRoute.path
75
+ } else {
76
+ // Hardcoded absolute construction preserving existing version structure
77
+ const vPath = currentRoute.version ? `/${currentRoute.version}` : ''
78
+ const lPath = locale === i18n.defaultLocale ? '' : `/${locale}`
79
+ targetPath = `${safeBase}${vPath}${lPath}` || '/'
80
+ }
81
+ }
82
+ } else {
83
+ // External Context Logic: simply rewrite the current absolute path
84
+ // Extract pure relative component by stripping existing locale prefix
85
+ let rawExternal = currentRoute.path
86
+
87
+ // Strip existing locale if any
88
+ const parts = rawExternal.split('/').filter(Boolean)
89
+ if (
90
+ parts.length > 0 &&
91
+ (Array.isArray(i18n.locales)
92
+ ? i18n.locales.includes(parts[0])
93
+ : !!i18n.locales[parts[0]])
94
+ ) {
95
+ // Already prefixed external route like /es/about -> become /about
96
+ parts.shift()
97
+ rawExternal = '/' + parts.join('/')
98
+ }
99
+
100
+ // Re-apply new locale
101
+ if (locale === i18n.defaultLocale) {
102
+ targetPath = rawExternal === '' ? '/' : rawExternal
103
+ } else {
104
+ const cleanExt = rawExternal.startsWith('/')
105
+ ? rawExternal
106
+ : `/${rawExternal}`
107
+ targetPath = `/${locale}${cleanExt === '/' ? '' : cleanExt}`
108
+ }
70
109
  }
71
110
  } else {
72
- // Fallback for when we don't have a current route (e.g. 404 page)
73
- // Try to find the root documentation page for the target locale
111
+ // Case B: Fallback for Unknown / 404 page
112
+ // Try to find first available page that matches the intended combo
74
113
  const targetRoute = allRoutes.find(
75
114
  (r) =>
76
- (r.filePath === 'index.mdx' || r.filePath === 'index.md') &&
77
115
  (r.locale || i18n.defaultLocale) === locale &&
78
- !r.version, // Prefer non-versioned root
116
+ (r.version || config.versions?.defaultVersion) ===
117
+ (currentVersion || config.versions?.defaultVersion),
79
118
  )
80
119
 
81
120
  if (targetRoute) {
82
121
  targetPath = targetRoute.path
83
122
  } else {
84
- targetPath = locale === i18n.defaultLocale ? '/' : `/${locale}`
123
+ const vPath =
124
+ currentVersion && currentVersion !== config.versions?.defaultVersion
125
+ ? `/${currentVersion}`
126
+ : ''
127
+ targetPath =
128
+ locale === i18n.defaultLocale
129
+ ? `${safeBase}${vPath}`
130
+ : `${safeBase}${vPath}/${locale}`
85
131
  }
86
132
  }
87
133
 
134
+ // Final safety check: cleanup double slashes and empty targets
135
+ if (!targetPath || targetPath === '') targetPath = '/'
136
+ targetPath = targetPath.replace(/\/+/g, '/')
137
+
88
138
  navigate(targetPath)
89
139
  }
90
140
 
91
- const currentLocaleConfig =
92
- config.i18n?.localeConfigs?.[currentLocale as string]
141
+ const locales = i18n?.locales
142
+ const defaultLabel = locales
143
+ ? Array.isArray(locales)
144
+ ? currentLocale
145
+ : (locales as Record<string, string>)[currentLocale as string]
146
+ : undefined
147
+
148
+ const currentLocaleConfig = i18n?.localeConfigs?.[currentLocale as string]
93
149
  const currentLocaleLabel =
94
- currentLocaleConfig?.label ||
95
- config.i18n?.locales[currentLocale as string] ||
96
- currentLocale
97
-
98
- const availableLocales = config.i18n
99
- ? Object.entries(config.i18n.locales).map(([key, defaultLabel]) => {
100
- const localeConfig = config.i18n?.localeConfigs?.[key]
101
- return {
102
- key,
103
- label: localeConfig?.label || defaultLabel,
104
- value: key,
105
- isCurrent: key === currentLocale,
106
- }
107
- })
108
- : []
150
+ currentLocaleConfig?.label || defaultLabel || currentLocale
151
+
152
+ const availableLocales = useMemo(() => {
153
+ return i18n
154
+ ? Array.isArray(i18n.locales)
155
+ ? i18n.locales.map((key) => {
156
+ const localeConfig = i18n?.localeConfigs?.[key]
157
+ return {
158
+ key: key as BoltdocsLocale,
159
+ label: localeConfig?.label || key,
160
+ value: key,
161
+ isCurrent: key === currentLocale,
162
+ }
163
+ })
164
+ : Object.entries(i18n.locales).map(([key, label]) => {
165
+ const localeConfig = i18n?.localeConfigs?.[key]
166
+ return {
167
+ key: key as BoltdocsLocale,
168
+ label: localeConfig?.label || label,
169
+ value: key,
170
+ isCurrent: key === currentLocale,
171
+ }
172
+ })
173
+ : []
174
+ }, [i18n, currentLocale])
109
175
 
110
176
  return {
111
177
  currentLocale,
@@ -6,30 +6,62 @@ import { useRoutes } from './use-routes'
6
6
  * Hook to automatically localize a path based on the current version and locale context.
7
7
  * It ensures that navigation preserves the active version and language across the entire site.
8
8
  */
9
- export function useLocalizedTo(to: RouterLinkProps['to']) {
9
+ export function useLocalizedTo(to: string): string
10
+ export function useLocalizedTo(to: RouterLinkProps['to']): RouterLinkProps['to']
11
+ export function useLocalizedTo(
12
+ to: RouterLinkProps['to'],
13
+ ): RouterLinkProps['to'] {
10
14
  const config = useConfig()
11
- const { currentLocale: activeLocale, currentVersion: activeVersion } =
12
- useRoutes()
15
+ const {
16
+ currentLocale: activeLocale,
17
+ currentVersion: activeVersion,
18
+ allRoutes,
19
+ } = useRoutes()
13
20
 
14
21
  if (!config || typeof to !== 'string') return to
15
22
 
16
- // External or absolute links don't need localization
17
- if (to.startsWith('http') || to.startsWith('//')) return to
23
+ // External, absolute, or anchor links don't need localization prefixing
24
+ if (
25
+ to.startsWith('http') ||
26
+ to.startsWith('//') ||
27
+ to.startsWith('#') ||
28
+ to.startsWith('site:')
29
+ ) {
30
+ return to.replace('site:', '')
31
+ }
32
+
33
+ // 0. Identify if the incoming path is explicitly registered as a known route
34
+ const [pathOnly, hashAndQuery] = to.split(/([?#].*)/s)
35
+ const normalizedTo =
36
+ pathOnly.endsWith('/') && pathOnly.length > 1
37
+ ? pathOnly.slice(0, -1)
38
+ : pathOnly
39
+
40
+ const isKnownRoute = allRoutes?.some((r) => {
41
+ const rp =
42
+ r.path.endsWith('/') && r.path.length > 1 ? r.path.slice(0, -1) : r.path
43
+ return rp === (normalizedTo || '/')
44
+ })
18
45
 
19
46
  const i18n = config.i18n
20
47
  const versions = config.versions
48
+ const base = (config.base || '/docs').replace(/\/$/, '')
49
+ const baseSegment = base.startsWith('/') ? base.substring(1) : base
21
50
 
22
- if (!i18n && !versions) return to
51
+ const rawParts = pathOnly.split('/').filter(Boolean)
23
52
 
24
- // 1. Identify the input intent
25
- const isDocLink = to.startsWith('/docs')
53
+ // Classify: it's a Doc Path if it explicitly contains base segment,
54
+ // OR if it's an 'unknown' path (backward compatible fallback assumes unknown = doc).
55
+ const hasExplicitBase =
56
+ baseSegment && rawParts.length > 0 && rawParts[0] === baseSegment
57
+ const isDocsPath = hasExplicitBase || (!isKnownRoute && rawParts.length > 0)
26
58
 
27
59
  // 3. Clean the 'to' path of ANY existing prefixes to avoid stacking
28
- const parts = to.split('/').filter(Boolean)
60
+ const parts = [...rawParts]
29
61
  let pIdx = 0
30
62
 
31
- // Strip '/docs' if present at start
32
- if (parts[pIdx] === 'docs') pIdx++
63
+ // Strip base segment if present at start
64
+ if (baseSegment && parts[pIdx] === baseSegment) pIdx++
33
65
 
34
66
  // Strip versions if present
35
67
  if (versions && parts.length > pIdx) {
@@ -38,33 +70,44 @@ export function useLocalizedTo(to: RouterLinkProps['to']) {
38
70
  }
39
71
 
40
72
  // Strip locales if present
41
- if (i18n && parts.length > pIdx && i18n.locales[parts[pIdx]]) pIdx++
73
+ const isLocale =
74
+ i18n &&
75
+ parts.length > pIdx &&
76
+ (Array.isArray(i18n.locales)
77
+ ? i18n.locales.includes(parts[pIdx])
78
+ : parts[pIdx] in i18n.locales)
79
+ if (isLocale) pIdx++
42
80
 
43
81
  // The actual relative route remaining
44
82
  const routeContent = parts.slice(pIdx)
45
83
 
46
- // 4. Reconstruct strictly from base
84
+ // 4. Reconstruct dynamically based on context
47
85
  const resultParts: string[] = []
48
86
 
49
- if (isDocLink) {
50
- resultParts.push('docs')
51
- if (versions && activeVersion) {
52
- resultParts.push(activeVersion)
53
- }
54
- }
55
-
56
- if (i18n && activeLocale) {
57
- resultParts.push(activeLocale)
87
+ if (isDocsPath) {
88
+ // Reconstruct DOCS path: /base/version/locale/content
89
+ if (baseSegment) resultParts.push(baseSegment)
90
+ if (versions && activeVersion) resultParts.push(activeVersion)
91
+ if (i18n && activeLocale) resultParts.push(activeLocale)
92
+ } else {
93
+ // Reconstruct EXTERNAL path: /locale/content
94
+ if (i18n && activeLocale) resultParts.push(activeLocale)
58
95
  }
59
96
 
60
97
  resultParts.push(...routeContent)
61
98
 
62
- const finalPath = `/${resultParts.join('/')}`
99
+ let finalPath = `/${resultParts.join('/')}`
63
100
 
64
- // Cleanup trailing slashes unless it's just root
65
- if (finalPath.length > 1 && finalPath.endsWith('/')) {
66
- return finalPath.slice(0, -1)
101
+ // Preserve trailing slash if present in input and output isn't just root
102
+ if (
103
+ pathOnly.endsWith('/') &&
104
+ pathOnly.length > 1 &&
105
+ !finalPath.endsWith('/')
106
+ ) {
107
+ finalPath += '/'
67
108
  }
68
109
 
69
- return finalPath || '/'
110
+ // Restore original query/hash
111
+ const result = (finalPath || '/') + (hashAndQuery || '')
112
+ return result
70
113
  }