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
@@ -1,29 +1,23 @@
1
1
  import { useEffect, useMemo } from 'react'
2
- import type { ComponentType, ReactNode } from 'react'
3
- import { Outlet, useLocation, useNavigate } from 'react-router-dom'
4
- import { RouterProvider } from 'react-aria-components'
2
+ import { Outlet, useLocation } from 'react-router-dom'
5
3
  import { BoltdocsProvider, useBoltdocsContext } from '../store/boltdocs-context'
6
4
  import { ThemeProvider } from '../app/theme-context'
7
5
  import { MdxComponentsProvider } from '../app/mdx-components-context'
8
- import * as ReactHelmetAsync from 'react-helmet-async'
6
+ import { HelmetProvider } from '../app/helmet-compat'
9
7
  import { ConfigContext } from '../app/config-context'
10
8
  import { ScrollHandler } from '../app/scroll-handler'
11
9
  import { mdxComponentsDefault } from '../app/mdx-component'
12
10
  import { RoutesProvider } from '../app/routes-context'
13
11
  import type { BoltdocsConfig } from '../../shared/types'
14
12
  import type { ComponentRoute } from '../types'
13
+ import { UIProvider } from '../app/ui-context'
15
14
 
16
15
  import virtualCustomComponents from 'virtual:boltdocs-mdx-components'
17
16
 
18
- type HelmetProviderModule = {
19
- HelmetProvider?: ComponentType<{ children?: ReactNode }>
20
- default?: { HelmetProvider?: ComponentType<{ children?: ReactNode }> }
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
21
20
  }
22
- const helmetProviderModule = ReactHelmetAsync as unknown as HelmetProviderModule
23
- const HelmetProvider =
24
- helmetProviderModule.HelmetProvider
25
- || helmetProviderModule.default?.HelmetProvider
26
- || (({ children }) => <>{children}</>)
27
21
 
28
22
  /**
29
23
  * Updates the HTML lang and dir attributes based on the current locale configuration.
@@ -44,56 +38,34 @@ function I18nUpdater({ config }: { config: BoltdocsConfig }) {
44
38
 
45
39
  /**
46
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().
47
42
  */
48
- function StoreSync({ config }: { config: BoltdocsConfig }) {
43
+ function StoreSync({
44
+ config,
45
+ routeMap,
46
+ }: {
47
+ config: BoltdocsConfig
48
+ routeMap: Map<string, ComponentRoute>
49
+ }) {
49
50
  const location = useLocation()
50
- const { setLocale, setVersion, currentLocale, currentVersion } =
51
- useBoltdocsContext()
51
+ const { setLocale, setVersion } = useBoltdocsContext()
52
52
 
53
53
  useEffect(() => {
54
- const parts = location.pathname.split('/').filter(Boolean)
55
- let cIdx = 0
56
- let detectedVersion = config.versions?.defaultVersion
57
- let detectedLocale = config.i18n?.defaultLocale
58
-
59
- // 0. Skip docs prefix if present
60
- if (parts[cIdx] === 'docs') cIdx++
61
-
62
- // 1. Version detection
63
- if (config.versions && parts.length > cIdx) {
64
- const versionMatch = config.versions.versions.find(
65
- (v) => v.path === parts[cIdx],
66
- )
67
- if (versionMatch) {
68
- detectedVersion = versionMatch.path
69
- cIdx++
70
- }
71
- }
54
+ const currentPath = normalizePath(location.pathname)
55
+ const matchedRoute = routeMap.get(currentPath)
72
56
 
73
- // 2. Locale detection
74
- if (config.i18n && parts.length > cIdx) {
75
- const potentialLocale = parts[cIdx]
76
- const isLocale = Array.isArray(config.i18n.locales)
77
- ? config.i18n.locales.includes(potentialLocale)
78
- : !!config.i18n.locales[potentialLocale]
79
-
80
- if (isLocale) {
81
- detectedLocale = potentialLocale
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)
82
66
  }
83
- } else if (config.i18n && parts.length === 0) {
84
- detectedLocale = currentLocale || config.i18n.defaultLocale
85
67
  }
86
-
87
- if (detectedLocale !== currentLocale) setLocale(detectedLocale || '')
88
- if (detectedVersion !== currentVersion) setVersion(detectedVersion ?? '')
89
- }, [
90
- location.pathname,
91
- config,
92
- setLocale,
93
- setVersion,
94
- currentLocale,
95
- currentVersion,
96
- ])
68
+ }, [location.pathname, config, routeMap, setLocale, setVersion])
97
69
 
98
70
  return null
99
71
  }
@@ -116,42 +88,59 @@ export function BoltdocsShell({
116
88
  [components],
117
89
  )
118
90
 
119
- const navigate = useNavigate()
120
91
  const { pathname } = useLocation()
121
-
122
- const currentPath = useMemo(() => {
123
- const p = pathname || '/'
124
- return p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p
125
- }, [pathname])
126
-
127
- const currentRoute = useMemo(() =>
128
- routes.find((r) => {
129
- const rp = r.path === '' ? '/' : r.path
130
- const normalize = (path: string) => path.endsWith('/') && path.length > 1 ? path.slice(0, -1) : path
131
- return normalize(rp) === currentPath
132
- }),
133
- [routes, currentPath]
134
- )
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])
135
124
 
136
125
  return (
137
126
  <HelmetProvider>
138
127
  <RoutesProvider routes={routes}>
139
128
  <ThemeProvider>
140
- <MdxComponentsProvider components={allComponents}>
141
- <ConfigContext.Provider value={config}>
142
- <RouterProvider navigate={navigate}>
129
+ <UIProvider>
130
+ <MdxComponentsProvider components={allComponents}>
131
+ <ConfigContext.Provider value={config}>
143
132
  <ScrollHandler />
144
133
  <BoltdocsProvider
145
- initialLocale={currentRoute?.locale}
146
- initialVersion={currentRoute?.version}
134
+ initialLocale={initialData.initLocale}
135
+ initialVersion={initialData.initVersion}
147
136
  >
148
- <StoreSync config={config} />
137
+ <StoreSync config={config} routeMap={routeMap} />
149
138
  <I18nUpdater config={config} />
150
139
  <Outlet />
151
140
  </BoltdocsProvider>
152
- </RouterProvider>
153
- </ConfigContext.Provider>
154
- </MdxComponentsProvider>
141
+ </ConfigContext.Provider>
142
+ </MdxComponentsProvider>
143
+ </UIProvider>
155
144
  </ThemeProvider>
156
145
  </RoutesProvider>
157
146
  </HelmetProvider>
@@ -2,57 +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
- const keys = Object.keys(modules)
27
- if (keys.length === 0) return undefined
28
-
29
- // Detect docs directory from keys (e.g., "/docs/...")
30
- const firstKey = keys[0].replace(/\\/g, '/')
31
- const parts = firstKey.split('/').filter(Boolean)
32
- const docsDirName = parts[0] || 'docs'
33
-
34
- const targetKey = `/${docsDirName}/${normalizedFilePath}`
35
- const targetKeyAlt = `./${docsDirName}/${normalizedFilePath}`
36
-
37
- return keys.find((key) => {
38
- const k = key.replace(/\\/g, '/')
39
- return k === targetKey || k === targetKeyAlt || k.endsWith(targetKey)
40
- })
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} />
41
61
  }
42
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
+
43
73
  export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
44
74
  const {
45
75
  routesData,
46
76
  config,
47
77
  mdxModules,
48
- Layout,
49
- homePage: HomePage,
50
78
  externalPages,
51
79
  externalLayout,
52
80
  components,
53
81
  } = options
54
82
 
55
- const EffectiveExternalLayout = externalLayout || Layout
83
+ const EffectiveExternalLayout =
84
+ externalLayout || (({ children }: any) => <>{children}</>)
56
85
 
57
86
  const withBase = (path: string) => {
58
87
  // Future support for base path in config
@@ -63,25 +92,103 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
63
92
  return `${b}${p}` || '/'
64
93
  }
65
94
 
66
- const allMetadata: ComponentRoute[] = [...routesData]
95
+ const defaultVersionMetadata: ComponentRoute[] = []
67
96
 
68
- // 1. Documentation routes
69
- const docRoutes: RouteRecord[] = routesData.map((route) => {
70
- const moduleKey = findModuleKey(mdxModules, route.filePath)
71
- const MDXComponent = moduleKey ? mdxModules[moduleKey]?.default : null
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(/\/$/, '')
72
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(/\/$/, '') || '/'
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
73
173
  const path = withBase(route.path === '' ? '/' : route.path)
74
174
 
75
175
  return {
76
176
  path,
77
177
  element: (
78
- <MdxPage MDXComponent={MDXComponent} mdxComponents={components} />
178
+ <MdxRouteElement
179
+ key={moduleKey || path}
180
+ moduleKey={moduleKey}
181
+ moduleLoader={moduleLoader}
182
+ route={route}
183
+ components={components}
184
+ />
79
185
  ),
80
186
  loader: async () => ({
81
187
  path,
82
188
  frontmatter: {
83
189
  title: route.title,
84
190
  description: route.description || '',
191
+ ...(route.frontmatter || {}),
85
192
  },
86
193
  headings: route.headings || [],
87
194
  filePath: route.filePath,
@@ -89,59 +196,148 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
89
196
  version: route.version,
90
197
  group: route.group,
91
198
  groupTitle: route.groupTitle,
199
+ date: route.date,
200
+ lastUpdated: route.lastUpdated,
92
201
  }),
93
202
  getStaticPaths: () => [path],
94
203
  }
95
204
  })
96
205
 
97
- 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
+ }> = []
223
+
224
+ // Insert base root always
225
+ targetBasePaths.push({
226
+ path: baseDocsPath,
227
+ filter: () => true, // Take first available doc generally
228
+ })
98
229
 
99
- // 2. Home page route
100
- if (HomePage) {
101
- const homeConfigs = [{ path: withBase('/'), locale: config.i18n?.defaultLocale }]
102
- if (config.i18n) {
103
- Object.keys(config.i18n.locales).forEach((locale) => {
104
- homeConfigs.push({ path: withBase(`/${locale}`), locale })
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}`)
105
243
  })
106
- }
244
+ })
245
+ }
107
246
 
108
- homeConfigs.forEach(({ path, locale }) => {
109
- // Avoid duplicate routes if documentation also maps to '/'
110
- if (!children.find((r) => r.path === path)) {
111
- allMetadata.push({
112
- path,
113
- locale,
114
- title: 'Home',
115
- filePath: '',
116
- headings: [],
117
- } as any)
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
+ })
118
255
 
119
- children.push({
120
- path,
121
- element: (
122
- <EffectiveExternalLayout>
123
- <HomePage />
124
- </EffectiveExternalLayout>
125
- ),
126
- loader: async () => ({
127
- path,
128
- locale,
129
- }),
130
- getStaticPaths: () => [path],
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
+ )
131
273
  })
132
274
  }
133
275
  })
134
276
  }
135
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
+
136
327
  // 3. External pages
328
+ const externalMetadata: ComponentRoute[] = []
137
329
  if (externalPages) {
138
330
  Object.entries(externalPages).forEach(([rawPath, ExtComponent]) => {
139
- const path = withBase(rawPath)
331
+ // Use the raw path directly (do not prefix with base docs path)
332
+ const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`
140
333
  if (!children.find((r) => r.path === path)) {
141
- allMetadata.push({
334
+ externalMetadata.push({
142
335
  path,
143
336
  locale: config.i18n?.defaultLocale,
144
- title: rawPath,
337
+ title:
338
+ rawPath === '/'
339
+ ? 'Home'
340
+ : rawPath.replace(/^\//, '').split('/').pop() || 'Page',
145
341
  filePath: '',
146
342
  headings: [],
147
343
  } as any)
@@ -160,14 +356,12 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
160
356
  getStaticPaths: () => [path],
161
357
  })
162
358
 
163
- // 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)
164
360
  if (config.i18n) {
165
361
  Object.keys(config.i18n.locales).forEach((locale) => {
166
- const localePath = withBase(
167
- `/${locale}${rawPath === '/' ? '' : rawPath}`,
168
- )
362
+ const localePath = `/${locale}${rawPath === '/' ? '' : rawPath}`
169
363
  if (!children.find((r) => r.path === localePath)) {
170
- allMetadata.push({
364
+ externalMetadata.push({
171
365
  path: localePath,
172
366
  locale,
173
367
  title: rawPath,
@@ -200,11 +394,13 @@ export function createRoutes(options: CreateRoutesOptions): RouteRecord[] {
200
394
  path: '*',
201
395
  element: (
202
396
  <EffectiveExternalLayout>
203
- <NotFound />
397
+ <NotFoundWrapper />
204
398
  </EffectiveExternalLayout>
205
399
  ),
206
400
  })
207
401
 
402
+ const allMetadata = [...docMetadata, ...externalMetadata]
403
+
208
404
  // Wrap everything in the Boltdocs shell (providers)
209
405
  return [
210
406
  {
@@ -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}