boltdocs 2.6.1 → 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 +173 -1328
  9. package/dist/client/index.d.ts +172 -1327
  10. package/dist/client/index.js +1 -1
  11. package/dist/{package-c99Cs7mD.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 +66 -13
  40. package/dist/node/index.d.mts +66 -14
  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-DukYeKmD.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 +120 -53
  129. package/src/client/hooks/use-localized-to.ts +70 -30
  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 +64 -81
  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 -77
  136. package/src/client/hooks/use-tabs.ts +3 -4
  137. package/src/client/hooks/use-version.ts +46 -18
  138. package/src/client/index.ts +13 -86
  139. package/src/client/mdx.ts +2 -0
  140. package/src/client/primitives.ts +19 -0
  141. package/src/client/ssg/boltdocs-shell.tsx +78 -57
  142. package/src/client/ssg/create-routes.tsx +290 -50
  143. package/src/client/ssg/mdx-page.tsx +2 -1
  144. package/src/client/store/boltdocs-context.tsx +83 -12
  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 +97 -21
  152. package/dist/node-CWN8U_p8.mjs +0 -88
  153. package/dist/node-D5iosYXv.cjs +0 -88
  154. package/dist/search-dialog-3lvKsbVG.js +0 -6
  155. package/dist/search-dialog-DMK5OpgH.cjs +0 -6
  156. package/dist/use-search-C9bxCqfF.js +0 -6
  157. package/dist/use-search-DcfZSunO.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 -76
  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
@@ -1,19 +1,24 @@
1
1
  import { useEffect, useMemo } from 'react'
2
- import { Outlet, useLocation, useNavigate } from 'react-router-dom'
3
- import { RouterProvider } from 'react-aria-components'
2
+ import { Outlet, useLocation } from 'react-router-dom'
4
3
  import { BoltdocsProvider, useBoltdocsContext } from '../store/boltdocs-context'
5
4
  import { ThemeProvider } from '../app/theme-context'
6
5
  import { MdxComponentsProvider } from '../app/mdx-components-context'
7
- import { HelmetProvider } from 'react-helmet-async'
6
+ import { HelmetProvider } from '../app/helmet-compat'
8
7
  import { ConfigContext } from '../app/config-context'
9
8
  import { ScrollHandler } from '../app/scroll-handler'
10
9
  import { mdxComponentsDefault } from '../app/mdx-component'
11
10
  import { RoutesProvider } from '../app/routes-context'
12
11
  import type { BoltdocsConfig } from '../../shared/types'
13
12
  import type { ComponentRoute } from '../types'
13
+ import { UIProvider } from '../app/ui-context'
14
14
 
15
15
  import virtualCustomComponents from 'virtual:boltdocs-mdx-components'
16
16
 
17
+ /** Normalize a path: strip trailing slash unless it is exactly '/'. */
18
+ function normalizePath(p: string): string {
19
+ return p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p
20
+ }
21
+
17
22
  /**
18
23
  * Updates the HTML lang and dir attributes based on the current locale configuration.
19
24
  */
@@ -33,53 +38,34 @@ function I18nUpdater({ config }: { config: BoltdocsConfig }) {
33
38
 
34
39
  /**
35
40
  * Synchronizes the Zustand store with the current URL pathname.
41
+ * Receives a pre-built Map for O(1) route lookups instead of O(n) .find().
36
42
  */
37
- function StoreSync({ config }: { config: BoltdocsConfig }) {
43
+ function StoreSync({
44
+ config,
45
+ routeMap,
46
+ }: {
47
+ config: BoltdocsConfig
48
+ routeMap: Map<string, ComponentRoute>
49
+ }) {
38
50
  const location = useLocation()
39
- const { setLocale, setVersion, currentLocale, currentVersion } =
40
- useBoltdocsContext()
51
+ const { setLocale, setVersion } = useBoltdocsContext()
41
52
 
42
53
  useEffect(() => {
43
- const parts = location.pathname.split('/').filter(Boolean)
44
- let cIdx = 0
45
- let detectedVersion = config.versions?.defaultVersion
46
- let detectedLocale = config.i18n?.defaultLocale
47
-
48
- // 0. Skip docs prefix if present
49
- if (parts[cIdx] === 'docs') cIdx++
50
-
51
- // 1. Version detection
52
- if (config.versions && parts.length > cIdx) {
53
- const versionMatch = config.versions.versions.find(
54
- (v) => v.path === parts[cIdx],
55
- )
56
- if (versionMatch) {
57
- detectedVersion = versionMatch.path
58
- cIdx++
59
- }
60
- }
54
+ const currentPath = normalizePath(location.pathname)
55
+ const matchedRoute = routeMap.get(currentPath)
61
56
 
62
- // 2. Locale detection
63
- if (
64
- config.i18n &&
65
- parts.length > cIdx &&
66
- config.i18n.locales[parts[cIdx]]
67
- ) {
68
- detectedLocale = parts[cIdx]
69
- } else if (config.i18n && parts.length === 0) {
70
- detectedLocale = currentLocale || config.i18n.defaultLocale
57
+ if (matchedRoute) {
58
+ if (config.i18n) {
59
+ const targetLocale = matchedRoute.locale || config.i18n.defaultLocale
60
+ setLocale(targetLocale)
61
+ }
62
+ if (config.versions) {
63
+ const targetVersion =
64
+ matchedRoute.version || config.versions.defaultVersion
65
+ setVersion(targetVersion)
66
+ }
71
67
  }
72
-
73
- if (detectedLocale !== currentLocale) setLocale(detectedLocale || '')
74
- if (detectedVersion !== currentVersion) setVersion(detectedVersion ?? '')
75
- }, [
76
- location.pathname,
77
- config,
78
- setLocale,
79
- setVersion,
80
- currentLocale,
81
- currentVersion,
82
- ])
68
+ }, [location.pathname, config, routeMap, setLocale, setVersion])
83
69
 
84
70
  return null
85
71
  }
@@ -102,26 +88,61 @@ export function BoltdocsShell({
102
88
  [components],
103
89
  )
104
90
 
105
- const navigate = useNavigate()
91
+ const { pathname } = useLocation()
92
+
93
+ const currentPath = useMemo(() => normalizePath(pathname || '/'), [pathname])
94
+
95
+ // Build a single O(1) lookup Map from the routes array.
96
+ // This replaces the 3 separate O(n) .find() calls that previously ran on every render.
97
+ const routeMap = useMemo(() => {
98
+ const map = new Map<string, ComponentRoute>()
99
+ for (const r of routes) {
100
+ const key = normalizePath(r.path === '' ? '/' : r.path)
101
+ map.set(key, r)
102
+ }
103
+ return map
104
+ }, [routes])
105
+
106
+ // Calculate frame-perfect initial values derived AUTHORITATIVELY from the static route match
107
+ const initialData = useMemo(() => {
108
+ const matched = routeMap.get(currentPath)
109
+
110
+ let initLocale = undefined
111
+ let initVersion = undefined
112
+
113
+ if (matched) {
114
+ if (config.i18n) {
115
+ initLocale = matched.locale || config.i18n.defaultLocale
116
+ }
117
+ if (config.versions) {
118
+ initVersion = matched.version || config.versions.defaultVersion
119
+ }
120
+ }
121
+
122
+ return { initLocale, initVersion }
123
+ }, [currentPath, config, routeMap])
106
124
 
107
125
  return (
108
126
  <HelmetProvider>
109
- <BoltdocsProvider>
127
+ <RoutesProvider routes={routes}>
110
128
  <ThemeProvider>
111
- <MdxComponentsProvider components={allComponents}>
112
- <ConfigContext.Provider value={config}>
113
- <RoutesProvider routes={routes}>
114
- <RouterProvider navigate={navigate}>
115
- <ScrollHandler />
116
- <StoreSync config={config} />
129
+ <UIProvider>
130
+ <MdxComponentsProvider components={allComponents}>
131
+ <ConfigContext.Provider value={config}>
132
+ <ScrollHandler />
133
+ <BoltdocsProvider
134
+ initialLocale={initialData.initLocale}
135
+ initialVersion={initialData.initVersion}
136
+ >
137
+ <StoreSync config={config} routeMap={routeMap} />
117
138
  <I18nUpdater config={config} />
118
139
  <Outlet />
119
- </RouterProvider>
120
- </RoutesProvider>
121
- </ConfigContext.Provider>
122
- </MdxComponentsProvider>
140
+ </BoltdocsProvider>
141
+ </ConfigContext.Provider>
142
+ </MdxComponentsProvider>
143
+ </UIProvider>
123
144
  </ThemeProvider>
124
- </BoltdocsProvider>
145
+ </RoutesProvider>
125
146
  </HelmetProvider>
126
147
  )
127
148
  }
@@ -2,47 +2,86 @@ import type { RouteRecord } from '@bdocs/ssg'
2
2
  import type { ComponentRoute, BoltdocsConfig } from '../types'
3
3
  import { MdxPage } from './mdx-page'
4
4
  import { BoltdocsShell } from './boltdocs-shell'
5
- import { NotFound } from '../components/ui-base/not-found'
5
+ import { NotFound } from '../components/ui-base'
6
+ const Loading = () => <div className="text-muted text-sm py-4">Loading...</div>
7
+ import type React from 'react'
8
+ import { useEffect } from 'react'
9
+ import { Navigate } from 'react-router-dom'
6
10
 
7
11
  interface CreateRoutesOptions {
8
12
  routesData: ComponentRoute[]
9
13
  config: BoltdocsConfig
10
- mdxModules: Record<string, { default?: React.ComponentType }>
14
+ mdxModules: Record<string, any>
11
15
  Layout: React.ComponentType<{ children: React.ReactNode }>
12
- homePage?: React.ComponentType
16
+
13
17
  externalPages?: Record<string, React.ComponentType>
14
18
  externalLayout?: React.ComponentType<{ children: React.ReactNode }>
15
19
  components?: Record<string, React.ComponentType>
16
20
  }
17
21
 
18
22
  /**
19
- * Finds the matching module key from import.meta.glob for a given filePath.
23
+ * Stable component to render MDX pages.
24
+ * By being outside createRoutes, it prevents React from unmounting the page on HMR.
20
25
  */
21
- function findModuleKey(
22
- modules: Record<string, any>,
23
- filePath: string,
24
- ): string | undefined {
25
- const normalizedFilePath = filePath.replace(/\\/g, '/')
26
- return Object.keys(modules).find(
27
- (key) =>
28
- key.endsWith(`/${normalizedFilePath}`) ||
29
- key.endsWith(normalizedFilePath),
30
- )
26
+ const MdxRouteElement = ({
27
+ moduleLoader,
28
+ moduleKey,
29
+ route,
30
+ components,
31
+ }: {
32
+ moduleLoader: any
33
+ moduleKey: string | undefined
34
+ route: ComponentRoute
35
+ components: any
36
+ }) => {
37
+ const MDXComponent = moduleLoader?.default ?? moduleLoader ?? null
38
+
39
+ useEffect(() => {
40
+ if (!import.meta.hot || !moduleKey) return
41
+
42
+ const handler = (data: { relPath: string }) => {
43
+ const incoming = data.relPath.replace(/\\/g, '/').replace(/^\//, '')
44
+ const routeFile = route.filePath.replace(/\\/g, '/').replace(/^\//, '')
45
+
46
+ if (incoming !== routeFile) return
47
+
48
+ const cacheBustUrl = moduleKey + '?t=' + Date.now()
49
+ import(/* @vite-ignore */ cacheBustUrl).then((m: any) => {
50
+ MDXComponent
51
+ })
52
+ }
53
+
54
+ import.meta.hot.on('boltdocs:mdx-update', handler)
55
+ return () => import.meta.hot?.off('boltdocs:mdx-update', handler)
56
+ }, [moduleKey, route.filePath])
57
+
58
+ if (!MDXComponent) return <Loading />
59
+
60
+ return <MdxPage MDXComponent={MDXComponent} mdxComponents={components} />
31
61
  }
32
62
 
63
+ import { useMdxComponents } from '../app/mdx-components-context'
64
+
65
+ const NotFoundWrapper = () => {
66
+ const components = useMdxComponents()
67
+ const ActiveNotFound = components.NotFound || components['404'] || NotFound
68
+ return <ActiveNotFound />
69
+ }
70
+
71
+ import { DocsLayout } from '../app/docs-layout'
72
+
33
73
  export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
34
74
  const {
35
75
  routesData,
36
76
  config,
37
77
  mdxModules,
38
- Layout,
39
- homePage: HomePage,
40
78
  externalPages,
41
79
  externalLayout,
42
80
  components,
43
81
  } = options
44
82
 
45
- const EffectiveExternalLayout = externalLayout || Layout
83
+ const EffectiveExternalLayout =
84
+ externalLayout || (({ children }: any) => <>{children}</>)
46
85
 
47
86
  const withBase = (path: string) => {
48
87
  // Future support for base path in config
@@ -53,23 +92,103 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
53
92
  return `${b}${p}` || '/'
54
93
  }
55
94
 
56
- // 1. Documentation routes
57
- const docRoutes: RouteRecord[] = routesData.map((route) => {
58
- const moduleKey = findModuleKey(mdxModules, route.filePath)
59
- const MDXComponent = moduleKey ? mdxModules[moduleKey]?.default : null
95
+ const defaultVersionMetadata: ComponentRoute[] = []
96
+
97
+ // Inject virtual explicit routes for default version to ensure paths like /docs/latest/... aren't 404s
98
+ const defaultVersion = config.versions?.defaultVersion
99
+ const docsBase = (config.base || '/docs').replace(/\/$/, '')
100
+
101
+ if (defaultVersion) {
102
+ routesData.forEach((route) => {
103
+ // If this route explicitly already belongs to a version, do not clone.
104
+ if (route.version) return
105
+
106
+ // Compute path without docs base prefix to properly place version token
107
+ const p = route.path || ''
108
+ const subPath = p.startsWith(docsBase)
109
+ ? p.substring(docsBase.length).replace(/^\//, '')
110
+ : p.replace(/^\//, '')
111
+
112
+ // Detect if it already includes the target version segment
113
+ const hasVersionPrefix =
114
+ subPath === defaultVersion || subPath.startsWith(`${defaultVersion}/`)
115
+
116
+ if (!hasVersionPrefix) {
117
+ // Standardize reconstruction: [docsBase] / [version] / [remaining_path]
118
+ const explicitPath =
119
+ `${docsBase}/${defaultVersion}/${subPath}`
120
+ .replace(/\/+/g, '/')
121
+ .replace(/\/$/, '') || '/'
60
122
 
123
+ defaultVersionMetadata.push({
124
+ ...route,
125
+ path: explicitPath,
126
+ version: defaultVersion,
127
+ })
128
+ }
129
+ })
130
+ }
131
+
132
+ const docMetadata = [...routesData, ...defaultVersionMetadata]
133
+
134
+ // 0. Build a single pre-computed lookup map for the MDX modules (O(N) build, O(1) access).
135
+ // This replaces the inner findModuleKey loops that executed an O(N) scan for EVERY route.
136
+ const moduleMap = new Map<string, string>()
137
+ const mdxModuleKeys = Object.keys(mdxModules)
138
+
139
+ if (mdxModuleKeys.length > 0) {
140
+ // Detect docs directory structure from keys (e.g., "/docs/intro.md")
141
+ const firstKeyNormalized = mdxModuleKeys[0].replace(/\\/g, '/')
142
+ const parts = firstKeyNormalized.split('/').filter(Boolean)
143
+ const docsDirName = parts[0] || 'docs'
144
+ const primaryPrefix = `/${docsDirName}/`
145
+ const altPrefix = `./${docsDirName}/`
146
+
147
+ for (const rawKey of mdxModuleKeys) {
148
+ const k = rawKey.replace(/\\/g, '/')
149
+ let relativePath = ''
150
+ if (k.indexOf(primaryPrefix) !== -1) {
151
+ relativePath = k.substring(
152
+ k.indexOf(primaryPrefix) + primaryPrefix.length,
153
+ )
154
+ } else if (k.startsWith(altPrefix)) {
155
+ relativePath = k.substring(altPrefix.length)
156
+ }
157
+
158
+ if (relativePath) {
159
+ moduleMap.set(relativePath, rawKey)
160
+ } else {
161
+ // Fallback: store full normalized key as a catch-all
162
+ moduleMap.set(k, rawKey)
163
+ }
164
+ }
165
+ }
166
+
167
+ // 1. Documentation routes
168
+ const docRoutes: RouteRecord[] = docMetadata.map((route) => {
169
+ // Perform constant-time lookup using the pre-computed map
170
+ const normalizedFilePath = route.filePath.replace(/\\/g, '/')
171
+ const moduleKey = moduleMap.get(normalizedFilePath)
172
+ const moduleLoader = moduleKey ? mdxModules[moduleKey] : null
61
173
  const path = withBase(route.path === '' ? '/' : route.path)
62
174
 
63
175
  return {
64
176
  path,
65
177
  element: (
66
- <MdxPage MDXComponent={MDXComponent} mdxComponents={components} />
178
+ <MdxRouteElement
179
+ key={moduleKey || path}
180
+ moduleKey={moduleKey}
181
+ moduleLoader={moduleLoader}
182
+ route={route}
183
+ components={components}
184
+ />
67
185
  ),
68
186
  loader: async () => ({
69
187
  path,
70
188
  frontmatter: {
71
189
  title: route.title,
72
190
  description: route.description || '',
191
+ ...(route.frontmatter || {}),
73
192
  },
74
193
  headings: route.headings || [],
75
194
  filePath: route.filePath,
@@ -77,43 +196,152 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
77
196
  version: route.version,
78
197
  group: route.group,
79
198
  groupTitle: route.groupTitle,
199
+ date: route.date,
200
+ lastUpdated: route.lastUpdated,
80
201
  }),
81
202
  getStaticPaths: () => [path],
82
203
  }
83
204
  })
84
205
 
85
- const children: RouteRecord[] = [...docRoutes]
206
+ // 2. Auto-fallback for the base paths (e.g. /docs, /docs/es) to the first documentation page
207
+ let baseDocsPath = (config.base || '/docs').replace(/\/$/, '')
208
+ if (!baseDocsPath) baseDocsPath = '/'
209
+
210
+ const locales = config.i18n?.locales
211
+ ? Array.isArray(config.i18n.locales)
212
+ ? config.i18n.locales
213
+ : Object.keys(config.i18n.locales)
214
+ : []
215
+
216
+ // 2a. Generate dynamic permutation matrix of version/locale combinations
217
+ const allVersions = config.versions?.versions?.map((v) => v.path) || []
218
+
219
+ const targetBasePaths: Array<{
220
+ path: string
221
+ filter: (p: string) => boolean
222
+ }> = []
86
223
 
87
- // 2. Home page route
88
- if (HomePage) {
89
- const homePaths = [withBase('/')]
90
- if (config.i18n) {
91
- Object.keys(config.i18n.locales).forEach((locale) => {
92
- homePaths.push(withBase(`/${locale}`))
224
+ // Insert base root always
225
+ targetBasePaths.push({
226
+ path: baseDocsPath,
227
+ filter: () => true, // Take first available doc generally
228
+ })
229
+
230
+ // Permutation builder: version loop nested with locale loop
231
+ // Ensures paths like /docs/v2.0, /docs/es, and /docs/v2.0/es ALL receive fallback logic.
232
+ const subPaths: string[] = []
233
+ if (allVersions.length > 0) {
234
+ allVersions.forEach((v) => subPaths.push(`/${v}`))
235
+ }
236
+ if (locales.length > 0) {
237
+ locales.forEach((l) => subPaths.push(`/${l}`))
238
+ }
239
+ if (allVersions.length > 0 && locales.length > 0) {
240
+ allVersions.forEach((v) => {
241
+ locales.forEach((l) => {
242
+ subPaths.push(`/${v}/${l}`)
93
243
  })
94
- }
244
+ })
245
+ }
95
246
 
96
- homePaths.forEach((path) => {
97
- // Avoid duplicate routes if documentation also maps to '/'
98
- if (!children.find((r) => r.path === path)) {
99
- children.push({
100
- path,
101
- element: (
102
- <EffectiveExternalLayout>
103
- <HomePage />
104
- </EffectiveExternalLayout>
105
- ),
106
- getStaticPaths: () => [path],
247
+ // Map permutations onto the physical base docs route
248
+ subPaths.forEach((sp) => {
249
+ const fullP = baseDocsPath === '/' ? sp : `${baseDocsPath}${sp}`
250
+ targetBasePaths.push({
251
+ path: fullP,
252
+ filter: (rp) => rp.startsWith(fullP.replace(/\/$/, '') + '/'),
253
+ })
254
+ })
255
+
256
+ // Pre-compute a Set of absolute and normalized path strings from the real routes
257
+ // to perform O(1) validation checks within the redirection loops below.
258
+ const docPathRegistry = new Set(
259
+ docRoutes.map((r) => (r.path || '').replace(/\/$/, '')),
260
+ )
261
+
262
+ // Pre-compute external pages paths so we do not hijack them with redirects
263
+ const externalPaths = new Set<string>()
264
+ if (externalPages) {
265
+ Object.keys(externalPages).forEach((rawPath) => {
266
+ const p = rawPath.startsWith('/') ? rawPath : `/${rawPath}`
267
+ externalPaths.add(p.replace(/\/$/, ''))
268
+ if (config.i18n) {
269
+ Object.keys(config.i18n.locales).forEach((locale) => {
270
+ externalPaths.add(
271
+ `/${locale}${p === '/' ? '' : p}`.replace(/\/$/, ''),
272
+ )
107
273
  })
108
274
  }
109
275
  })
110
276
  }
111
277
 
278
+ // 2b. Deploy smart redirects
279
+ targetBasePaths.forEach(({ path: bPath, filter }) => {
280
+ if (bPath === '/') return // Never hijack global app root
281
+
282
+ const normalizedPath = bPath.replace(/\/$/, '')
283
+ const hasExplicitMatch =
284
+ docPathRegistry.has(normalizedPath) || externalPaths.has(normalizedPath)
285
+
286
+ if (!hasExplicitMatch) {
287
+ const defaultTab = config.theme?.tabs?.[0]?.id
288
+ const defaultTabPath = defaultTab
289
+ ? `${normalizedPath}/${defaultTab}`.replace(/\/+/g, '/')
290
+ : null
291
+
292
+ // Prioritize: Find a real route that matches the default tab first, then fall back to the first route beginning with this pattern.
293
+ const matchedRoute =
294
+ defaultTabPath && docPathRegistry.has(defaultTabPath.replace(/\/$/, ''))
295
+ ? docRoutes.find(
296
+ (r) =>
297
+ r.path.replace(/\/$/, '') === defaultTabPath.replace(/\/$/, ''),
298
+ )
299
+ : docRoutes.find((r) => filter(r.path) && r.path !== normalizedPath)
300
+
301
+ // Ultimate fallback: the absolute first document
302
+ const finalTarget = matchedRoute
303
+ ? matchedRoute.path
304
+ : docRoutes.length > 0
305
+ ? docRoutes[0].path
306
+ : null
307
+
308
+ if (finalTarget) {
309
+ docRoutes.push({
310
+ path: bPath,
311
+ element: <Navigate to={finalTarget} replace />,
312
+ loader: async () => ({ path: bPath }),
313
+ getStaticPaths: () => [bPath],
314
+ })
315
+ }
316
+ }
317
+ })
318
+
319
+ // Group all documentation routes under the persistent DocsLayout
320
+ const docsLayoutRoute: RouteRecord = {
321
+ element: <DocsLayout />,
322
+ children: docRoutes,
323
+ }
324
+
325
+ const children: RouteRecord[] = [docsLayoutRoute]
326
+
112
327
  // 3. External pages
328
+ const externalMetadata: ComponentRoute[] = []
113
329
  if (externalPages) {
114
330
  Object.entries(externalPages).forEach(([rawPath, ExtComponent]) => {
115
- const path = withBase(rawPath)
331
+ // Use the raw path directly (do not prefix with base docs path)
332
+ const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`
116
333
  if (!children.find((r) => r.path === path)) {
334
+ externalMetadata.push({
335
+ path,
336
+ locale: config.i18n?.defaultLocale,
337
+ title:
338
+ rawPath === '/'
339
+ ? 'Home'
340
+ : rawPath.replace(/^\//, '').split('/').pop() || 'Page',
341
+ filePath: '',
342
+ headings: [],
343
+ } as any)
344
+
117
345
  children.push({
118
346
  path,
119
347
  element: (
@@ -121,16 +349,26 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
121
349
  <ExtComponent />
122
350
  </EffectiveExternalLayout>
123
351
  ),
352
+ loader: async () => ({
353
+ path,
354
+ locale: config.i18n?.defaultLocale,
355
+ }),
124
356
  getStaticPaths: () => [path],
125
357
  })
126
358
 
127
- // Also add i18n variants for external pages if needed
359
+ // Also add i18n variants for external pages if needed (do not prefix with base docs path)
128
360
  if (config.i18n) {
129
361
  Object.keys(config.i18n.locales).forEach((locale) => {
130
- const localePath = withBase(
131
- `/${locale}${rawPath === '/' ? '' : rawPath}`,
132
- )
362
+ const localePath = `/${locale}${rawPath === '/' ? '' : rawPath}`
133
363
  if (!children.find((r) => r.path === localePath)) {
364
+ externalMetadata.push({
365
+ path: localePath,
366
+ locale,
367
+ title: rawPath,
368
+ filePath: '',
369
+ headings: [],
370
+ } as any)
371
+
134
372
  children.push({
135
373
  path: localePath,
136
374
  element: (
@@ -138,6 +376,10 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
138
376
  <ExtComponent />
139
377
  </EffectiveExternalLayout>
140
378
  ),
379
+ loader: async () => ({
380
+ path: localePath,
381
+ locale,
382
+ }),
141
383
  getStaticPaths: () => [localePath],
142
384
  })
143
385
  }
@@ -152,14 +394,12 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
152
394
  path: '*',
153
395
  element: (
154
396
  <EffectiveExternalLayout>
155
- <NotFound />
397
+ <NotFoundWrapper />
156
398
  </EffectiveExternalLayout>
157
399
  ),
158
400
  })
159
401
 
160
- // --- 5. Construct Metadata for UI Providers ---
161
- // We need to pass the full metadata to BoltdocsShell so that Sidebar/Tabs can work.
162
- const allMetadata: ComponentRoute[] = [...routesData]
402
+ const allMetadata = [...docMetadata, ...externalMetadata]
163
403
 
164
404
  // Wrap everything in the Boltdocs shell (providers)
165
405
  return [
@@ -12,7 +12,6 @@ export function MdxPage({
12
12
  const data = useLoaderData() as any
13
13
  const MDXComponent = propMDX || data?.MDXComponent
14
14
  const components = propComponents || data?.mdxComponents
15
-
16
15
  if (!MDXComponent) return null
17
16
 
18
17
  return (
@@ -28,6 +27,8 @@ export function MdxPage({
28
27
  version: data.version,
29
28
  group: data.group,
30
29
  groupTitle: data.groupTitle,
30
+ lastUpdated: data.lastUpdated,
31
+ frontmatter: data.frontmatter,
31
32
  } as any
32
33
  }
33
34
  content={MDXComponent}