boltdocs 1.3.0 → 1.3.2

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 (103) hide show
  1. package/dist/{cache-EHR7SXRU.mjs → cache-GQHF6BXI.mjs} +1 -1
  2. package/dist/{chunk-GSYECEZY.mjs → chunk-CYBWLFOG.mjs} +5 -1
  3. package/dist/node/index.js +36 -20
  4. package/dist/node/index.mjs +34 -22
  5. package/package.json +1 -1
  6. package/src/client/app/index.tsx +344 -344
  7. package/src/client/app/preload.tsx +56 -56
  8. package/src/client/index.ts +40 -40
  9. package/src/client/ssr.tsx +51 -51
  10. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +76 -76
  11. package/src/client/theme/components/CodeBlock/index.ts +1 -1
  12. package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +154 -154
  13. package/src/client/theme/components/PackageManagerTabs/index.ts +1 -1
  14. package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +64 -64
  15. package/src/client/theme/components/Playground/Playground.tsx +124 -124
  16. package/src/client/theme/components/Playground/index.ts +1 -1
  17. package/src/client/theme/components/Playground/playground.css +168 -168
  18. package/src/client/theme/components/Video/Video.tsx +84 -84
  19. package/src/client/theme/components/Video/index.ts +1 -1
  20. package/src/client/theme/components/Video/video.css +41 -41
  21. package/src/client/theme/components/mdx/Admonition.tsx +80 -80
  22. package/src/client/theme/components/mdx/Badge.tsx +31 -31
  23. package/src/client/theme/components/mdx/Button.tsx +50 -50
  24. package/src/client/theme/components/mdx/Card.tsx +80 -80
  25. package/src/client/theme/components/mdx/List.tsx +57 -57
  26. package/src/client/theme/components/mdx/Tabs.tsx +94 -94
  27. package/src/client/theme/components/mdx/index.ts +18 -18
  28. package/src/client/theme/components/mdx/mdx-components.css +424 -424
  29. package/src/client/theme/icons/bun.tsx +62 -62
  30. package/src/client/theme/icons/deno.tsx +20 -20
  31. package/src/client/theme/icons/discord.tsx +12 -12
  32. package/src/client/theme/icons/github.tsx +15 -15
  33. package/src/client/theme/icons/npm.tsx +13 -13
  34. package/src/client/theme/icons/pnpm.tsx +72 -72
  35. package/src/client/theme/icons/twitter.tsx +12 -12
  36. package/src/client/theme/styles/markdown.css +343 -343
  37. package/src/client/theme/styles/variables.css +162 -162
  38. package/src/client/theme/styles.css +37 -37
  39. package/src/client/theme/ui/BackgroundGradient/BackgroundGradient.tsx +10 -10
  40. package/src/client/theme/ui/BackgroundGradient/index.ts +1 -1
  41. package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +68 -68
  42. package/src/client/theme/ui/Breadcrumbs/index.ts +1 -1
  43. package/src/client/theme/ui/Footer/footer.css +32 -32
  44. package/src/client/theme/ui/Head/Head.tsx +69 -69
  45. package/src/client/theme/ui/Head/index.ts +1 -1
  46. package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +125 -125
  47. package/src/client/theme/ui/LanguageSwitcher/index.ts +1 -1
  48. package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +98 -98
  49. package/src/client/theme/ui/Layout/Layout.tsx +202 -202
  50. package/src/client/theme/ui/Layout/base.css +76 -76
  51. package/src/client/theme/ui/Layout/index.ts +2 -2
  52. package/src/client/theme/ui/Layout/pagination.css +72 -72
  53. package/src/client/theme/ui/Layout/responsive.css +36 -36
  54. package/src/client/theme/ui/Link/Link.tsx +254 -254
  55. package/src/client/theme/ui/Link/index.ts +2 -2
  56. package/src/client/theme/ui/Loading/Loading.tsx +10 -10
  57. package/src/client/theme/ui/Loading/index.ts +1 -1
  58. package/src/client/theme/ui/Loading/loading.css +30 -30
  59. package/src/client/theme/ui/Navbar/GithubStars.tsx +27 -27
  60. package/src/client/theme/ui/Navbar/Navbar.tsx +145 -145
  61. package/src/client/theme/ui/Navbar/index.ts +2 -2
  62. package/src/client/theme/ui/Navbar/navbar.css +233 -233
  63. package/src/client/theme/ui/NotFound/NotFound.tsx +19 -19
  64. package/src/client/theme/ui/NotFound/index.ts +1 -1
  65. package/src/client/theme/ui/NotFound/not-found.css +64 -64
  66. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +235 -235
  67. package/src/client/theme/ui/OnThisPage/index.ts +1 -1
  68. package/src/client/theme/ui/OnThisPage/toc.css +132 -132
  69. package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +18 -18
  70. package/src/client/theme/ui/PoweredBy/index.ts +1 -1
  71. package/src/client/theme/ui/PoweredBy/powered-by.css +76 -76
  72. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +199 -199
  73. package/src/client/theme/ui/SearchDialog/index.ts +1 -1
  74. package/src/client/theme/ui/SearchDialog/search.css +152 -152
  75. package/src/client/theme/ui/Sidebar/Sidebar.tsx +204 -204
  76. package/src/client/theme/ui/Sidebar/index.ts +1 -1
  77. package/src/client/theme/ui/Sidebar/sidebar.css +236 -236
  78. package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +69 -69
  79. package/src/client/theme/ui/ThemeToggle/index.ts +1 -1
  80. package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +136 -136
  81. package/src/client/theme/ui/VersionSwitcher/index.ts +1 -1
  82. package/src/client/types.ts +50 -50
  83. package/src/client/utils.ts +26 -26
  84. package/src/node/cache.ts +408 -408
  85. package/src/node/config.ts +192 -192
  86. package/src/node/index.ts +21 -21
  87. package/src/node/mdx.ts +120 -120
  88. package/src/node/plugin/entry.ts +58 -58
  89. package/src/node/plugin/html.ts +55 -55
  90. package/src/node/plugin/index.ts +193 -193
  91. package/src/node/plugin/types.ts +11 -11
  92. package/src/node/routes/cache.ts +28 -28
  93. package/src/node/routes/index.ts +167 -167
  94. package/src/node/routes/parser.ts +153 -127
  95. package/src/node/routes/sorter.ts +42 -42
  96. package/src/node/routes/types.ts +49 -49
  97. package/src/node/ssg/index.ts +114 -114
  98. package/src/node/ssg/meta.ts +33 -34
  99. package/src/node/ssg/options.ts +13 -13
  100. package/src/node/ssg/sitemap.ts +55 -54
  101. package/src/node/utils.ts +145 -134
  102. package/tsconfig.json +20 -20
  103. package/tsup.config.ts +22 -22
@@ -1,344 +1,344 @@
1
- import React, { useEffect, useState } from "react";
2
- import ReactDOM from "react-dom/client";
3
- import {
4
- BrowserRouter,
5
- Routes,
6
- Route,
7
- Outlet,
8
- useLocation,
9
- } from "react-router-dom";
10
- import { ThemeLayout } from "../theme/ui/Layout";
11
- import { NotFound } from "../theme/ui/NotFound";
12
- import { Loading } from "../theme/ui/Loading";
13
- import { MDXProvider } from "@mdx-js/react";
14
- import { ComponentRoute, CreateBoltdocsAppOptions } from "../types";
15
- import {
16
- createContext,
17
- useContext,
18
- Suspense,
19
- lazy,
20
- useLayoutEffect,
21
- } from "react";
22
- import { Link as LucideLink } from "lucide-react";
23
-
24
- export const ConfigContext = createContext<any>(null);
25
-
26
- export function useConfig() {
27
- return useContext(ConfigContext);
28
- }
29
-
30
- const CodeBlock = lazy(() =>
31
- import("../theme/components/CodeBlock").then((m) => ({
32
- default: m.CodeBlock,
33
- })),
34
- );
35
- const Video = lazy(() =>
36
- import("../theme/components/Video").then((m) => ({ default: m.Video })),
37
- );
38
- const PackageManagerTabs = lazy(() =>
39
- import("../theme/components/PackageManagerTabs").then((m) => ({
40
- default: m.PackageManagerTabs,
41
- })),
42
- );
43
- declare global {
44
- interface ImportMeta {
45
- env: Record<string, any>;
46
- }
47
- }
48
-
49
- import { PreloadProvider } from "./preload";
50
-
51
- const Heading = ({
52
- level,
53
- id,
54
- children,
55
- }: {
56
- level: number;
57
- id?: string;
58
- children: React.ReactNode;
59
- }) => {
60
- const Tag = `h${level}` as keyof JSX.IntrinsicElements;
61
- return (
62
- <Tag id={id} className="boltdocs-heading">
63
- {children}
64
- {id && (
65
- <a href={`#${id}`} className="header-anchor" aria-label="Anchor">
66
- <LucideLink size={16} />
67
- </a>
68
- )}
69
- </Tag>
70
- );
71
- };
72
-
73
- const mdxComponents = {
74
- h1: (props: any) => <Heading level={1} {...props} />,
75
- h2: (props: any) => <Heading level={2} {...props} />,
76
- h3: (props: any) => <Heading level={3} {...props} />,
77
- h4: (props: any) => <Heading level={4} {...props} />,
78
- h5: (props: any) => <Heading level={5} {...props} />,
79
- h6: (props: any) => <Heading level={6} {...props} />,
80
- pre: (props: any) => {
81
- return (
82
- <Suspense fallback={<div className="code-block-skeleton" />}>
83
- <CodeBlock {...props}>{props.children}</CodeBlock>
84
- </Suspense>
85
- );
86
- },
87
- video: (props: any) => (
88
- <Suspense fallback={<div className="video-skeleton" />}>
89
- <Video {...props} />
90
- </Suspense>
91
- ),
92
- PackageManagerTabs: (props: any) => (
93
- <Suspense fallback={<div className="pkg-tabs-skeleton" />}>
94
- <PackageManagerTabs {...props} />
95
- </Suspense>
96
- ),
97
- };
98
-
99
- export function AppShell({
100
- initialRoutes,
101
- initialConfig,
102
- modules,
103
- hot,
104
- homePage: HomePage,
105
- components: customComponents = {},
106
- }: {
107
- initialRoutes: ComponentRoute[];
108
- initialConfig: any;
109
- modules: Record<string, () => Promise<any>>;
110
- hot?: any;
111
- homePage?: React.ComponentType;
112
- components?: Record<string, React.ComponentType<any>>;
113
- }) {
114
- const [routesInfo, setRoutesInfo] = useState<ComponentRoute[]>(initialRoutes);
115
- const [config] = useState(initialConfig);
116
-
117
- const resolveRoutes = (infos: ComponentRoute[]) => {
118
- return infos
119
- .filter(
120
- (route) => !(HomePage && (route.path === "/" || route.path === "")),
121
- )
122
- .map((route) => {
123
- const loaderKey = Object.keys(modules).find((k) =>
124
- k.endsWith("/" + route.filePath),
125
- );
126
- const loader = loaderKey ? modules[loaderKey] : null;
127
-
128
- return {
129
- ...route,
130
- Component: React.lazy(() => {
131
- if (!loader)
132
- return Promise.resolve({ default: () => <NotFound /> });
133
- return loader() as any;
134
- }),
135
- };
136
- });
137
- };
138
-
139
- const [resolvedRoutes, setResolvedRoutes] = useState<any[]>(() =>
140
- resolveRoutes(initialRoutes),
141
- );
142
-
143
- // Subscribe to HMR events
144
- useEffect(() => {
145
- if (hot) {
146
- hot.on("boltdocs:routes-update", (newRoutes: ComponentRoute[]) => {
147
- setRoutesInfo(newRoutes);
148
- });
149
- }
150
- }, [hot]);
151
-
152
- // Sync resolved routes when info or modules change
153
- useEffect(() => {
154
- setResolvedRoutes(resolveRoutes(routesInfo));
155
- }, [routesInfo, modules]);
156
-
157
- return (
158
- <ConfigContext.Provider value={config}>
159
- <PreloadProvider routes={routesInfo} modules={modules}>
160
- <ScrollHandler />
161
- <Routes>
162
- {/* Custom home page WITHOUT docs layout */}
163
- {HomePage && (
164
- <Route
165
- path="/"
166
- element={
167
- <ThemeLayout
168
- config={config}
169
- routes={routesInfo}
170
- sidebar={null}
171
- toc={null}
172
- breadcrumbs={null}
173
- {...config.themeConfig?.layoutProps}
174
- >
175
- <HomePage />
176
- </ThemeLayout>
177
- }
178
- />
179
- )}
180
-
181
- {/* Documentation pages WITH sidebar + TOC layout */}
182
- <Route
183
- key="docs-layout"
184
- element={<DocsLayout config={config} routes={routesInfo} />}
185
- >
186
- {resolvedRoutes.map((route: any) => (
187
- <Route
188
- key={route.path}
189
- path={route.path === "" ? "/" : route.path}
190
- element={
191
- <React.Suspense fallback={<Loading />}>
192
- <MdxPage
193
- Component={route.Component}
194
- customComponents={customComponents}
195
- />
196
- </React.Suspense>
197
- }
198
- />
199
- ))}
200
- </Route>
201
-
202
- <Route
203
- path="*"
204
- element={
205
- <ThemeLayout
206
- config={config}
207
- routes={routesInfo}
208
- {...config.themeConfig?.layoutProps}
209
- >
210
- <NotFound />
211
- </ThemeLayout>
212
- }
213
- />
214
- </Routes>
215
- </PreloadProvider>
216
- </ConfigContext.Provider>
217
- );
218
- }
219
-
220
- /**
221
- * Handles scroll restoration and hash scrolling on navigation.
222
- */
223
- function ScrollHandler() {
224
- const { pathname, hash } = useLocation();
225
-
226
- useLayoutEffect(() => {
227
- // Only scroll if we are not in a pending transition state (if we were using useTransition)
228
- // For now, we ensure the scroll happens.
229
- if (hash) {
230
- const id = hash.replace("#", "");
231
- const element = document.getElementById(id);
232
- if (element) {
233
- const offset = 80;
234
- const bodyRect = document.body.getBoundingClientRect().top;
235
- const elementRect = element.getBoundingClientRect().top;
236
- const elementPosition = elementRect - bodyRect;
237
- const offsetPosition = elementPosition - offset;
238
-
239
- window.scrollTo({
240
- top: offsetPosition,
241
- behavior: "smooth",
242
- });
243
- return;
244
- }
245
- }
246
- window.scrollTo(0, 0);
247
- }, [pathname, hash]);
248
-
249
- return null;
250
- }
251
-
252
- /** Wrapper layout for doc pages (sidebar + content + TOC) */
253
- function DocsLayout({
254
- config,
255
- routes,
256
- }: {
257
- config: any;
258
- routes: ComponentRoute[];
259
- }) {
260
- return (
261
- <ThemeLayout
262
- config={config}
263
- routes={routes}
264
- {...config.themeConfig?.layoutProps}
265
- >
266
- <Outlet />
267
- </ThemeLayout>
268
- );
269
- }
270
-
271
- /**
272
- * Renders an MDX page securely, injecting required custom components.
273
- * For example, this overrides the default `<pre>` HTML tags emitted by MDX
274
- * with the Boltdocs `CodeBlock` component for syntax highlighting.
275
- *
276
- * @param props - Contains the dynamically loaded React component representing the MDX page
277
- */
278
- function MdxPage({
279
- Component,
280
- customComponents = {},
281
- }: {
282
- Component: React.LazyExoticComponent<any>;
283
- customComponents?: Record<string, React.ComponentType<any>>;
284
- }) {
285
- const allComponents = { ...mdxComponents, ...customComponents };
286
- return (
287
- <MDXProvider components={allComponents}>
288
- <Component />
289
- </MDXProvider>
290
- );
291
- }
292
-
293
- /**
294
- * Creates and mounts the Boltdocs documentation app.
295
- *
296
- * Usage:
297
- * ```tsx
298
- * import { createBoltdocsApp } from 'boltdocs/client'
299
- * import routes from 'virtual:boltdocs-routes'
300
- * import config from 'virtual:boltdocs-config'
301
- * import 'boltdocs/style.css'
302
- * import HomePage from './HomePage'
303
- *
304
- * createBoltdocsApp({
305
- * target: '#root',
306
- * routes,
307
- * config,
308
- * modules: import.meta.glob('/docs/**\/*.{md,mdx}'),
309
- * hot: import.meta.hot,
310
- * homePage: HomePage,
311
- * })
312
- * ```
313
- */
314
- export function createBoltdocsApp(options: CreateBoltdocsAppOptions) {
315
- const { target, routes, config, modules, hot, homePage } = options;
316
- const container = document.querySelector(target);
317
- if (!container) {
318
- throw new Error(
319
- `[boltdocs] Mount target "${target}" not found in document.`,
320
- );
321
- }
322
-
323
- const app = (
324
- <React.StrictMode>
325
- <BrowserRouter>
326
- <AppShell
327
- initialRoutes={routes}
328
- initialConfig={config}
329
- modules={modules}
330
- hot={hot}
331
- homePage={homePage}
332
- components={options.components}
333
- />
334
- </BrowserRouter>
335
- </React.StrictMode>
336
- );
337
-
338
- // SSG pre-renders a shell with mock components for SEO crawlers.
339
- // We always use createRoot because the SSG output doesn't match the
340
- // real client-side component tree (components are lazy/dynamic).
341
- // Clear any SSG placeholder content before mounting.
342
- container.innerHTML = "";
343
- ReactDOM.createRoot(container as HTMLElement).render(app);
344
- }
1
+ import React, { useEffect, useState } from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import {
4
+ BrowserRouter,
5
+ Routes,
6
+ Route,
7
+ Outlet,
8
+ useLocation,
9
+ } from "react-router-dom";
10
+ import { ThemeLayout } from "../theme/ui/Layout";
11
+ import { NotFound } from "../theme/ui/NotFound";
12
+ import { Loading } from "../theme/ui/Loading";
13
+ import { MDXProvider } from "@mdx-js/react";
14
+ import { ComponentRoute, CreateBoltdocsAppOptions } from "../types";
15
+ import {
16
+ createContext,
17
+ useContext,
18
+ Suspense,
19
+ lazy,
20
+ useLayoutEffect,
21
+ } from "react";
22
+ import { Link as LucideLink } from "lucide-react";
23
+
24
+ export const ConfigContext = createContext<any>(null);
25
+
26
+ export function useConfig() {
27
+ return useContext(ConfigContext);
28
+ }
29
+
30
+ const CodeBlock = lazy(() =>
31
+ import("../theme/components/CodeBlock").then((m) => ({
32
+ default: m.CodeBlock,
33
+ })),
34
+ );
35
+ const Video = lazy(() =>
36
+ import("../theme/components/Video").then((m) => ({ default: m.Video })),
37
+ );
38
+ const PackageManagerTabs = lazy(() =>
39
+ import("../theme/components/PackageManagerTabs").then((m) => ({
40
+ default: m.PackageManagerTabs,
41
+ })),
42
+ );
43
+ declare global {
44
+ interface ImportMeta {
45
+ env: Record<string, any>;
46
+ }
47
+ }
48
+
49
+ import { PreloadProvider } from "./preload";
50
+
51
+ const Heading = ({
52
+ level,
53
+ id,
54
+ children,
55
+ }: {
56
+ level: number;
57
+ id?: string;
58
+ children: React.ReactNode;
59
+ }) => {
60
+ const Tag = `h${level}` as keyof JSX.IntrinsicElements;
61
+ return (
62
+ <Tag id={id} className="boltdocs-heading">
63
+ {children}
64
+ {id && (
65
+ <a href={`#${id}`} className="header-anchor" aria-label="Anchor">
66
+ <LucideLink size={16} />
67
+ </a>
68
+ )}
69
+ </Tag>
70
+ );
71
+ };
72
+
73
+ const mdxComponents = {
74
+ h1: (props: any) => <Heading level={1} {...props} />,
75
+ h2: (props: any) => <Heading level={2} {...props} />,
76
+ h3: (props: any) => <Heading level={3} {...props} />,
77
+ h4: (props: any) => <Heading level={4} {...props} />,
78
+ h5: (props: any) => <Heading level={5} {...props} />,
79
+ h6: (props: any) => <Heading level={6} {...props} />,
80
+ pre: (props: any) => {
81
+ return (
82
+ <Suspense fallback={<div className="code-block-skeleton" />}>
83
+ <CodeBlock {...props}>{props.children}</CodeBlock>
84
+ </Suspense>
85
+ );
86
+ },
87
+ video: (props: any) => (
88
+ <Suspense fallback={<div className="video-skeleton" />}>
89
+ <Video {...props} />
90
+ </Suspense>
91
+ ),
92
+ PackageManagerTabs: (props: any) => (
93
+ <Suspense fallback={<div className="pkg-tabs-skeleton" />}>
94
+ <PackageManagerTabs {...props} />
95
+ </Suspense>
96
+ ),
97
+ };
98
+
99
+ export function AppShell({
100
+ initialRoutes,
101
+ initialConfig,
102
+ modules,
103
+ hot,
104
+ homePage: HomePage,
105
+ components: customComponents = {},
106
+ }: {
107
+ initialRoutes: ComponentRoute[];
108
+ initialConfig: any;
109
+ modules: Record<string, () => Promise<any>>;
110
+ hot?: any;
111
+ homePage?: React.ComponentType;
112
+ components?: Record<string, React.ComponentType<any>>;
113
+ }) {
114
+ const [routesInfo, setRoutesInfo] = useState<ComponentRoute[]>(initialRoutes);
115
+ const [config] = useState(initialConfig);
116
+
117
+ const resolveRoutes = (infos: ComponentRoute[]) => {
118
+ return infos
119
+ .filter(
120
+ (route) => !(HomePage && (route.path === "/" || route.path === "")),
121
+ )
122
+ .map((route) => {
123
+ const loaderKey = Object.keys(modules).find((k) =>
124
+ k.endsWith("/" + route.filePath),
125
+ );
126
+ const loader = loaderKey ? modules[loaderKey] : null;
127
+
128
+ return {
129
+ ...route,
130
+ Component: React.lazy(() => {
131
+ if (!loader)
132
+ return Promise.resolve({ default: () => <NotFound /> });
133
+ return loader() as any;
134
+ }),
135
+ };
136
+ });
137
+ };
138
+
139
+ const [resolvedRoutes, setResolvedRoutes] = useState<any[]>(() =>
140
+ resolveRoutes(initialRoutes),
141
+ );
142
+
143
+ // Subscribe to HMR events
144
+ useEffect(() => {
145
+ if (hot) {
146
+ hot.on("boltdocs:routes-update", (newRoutes: ComponentRoute[]) => {
147
+ setRoutesInfo(newRoutes);
148
+ });
149
+ }
150
+ }, [hot]);
151
+
152
+ // Sync resolved routes when info or modules change
153
+ useEffect(() => {
154
+ setResolvedRoutes(resolveRoutes(routesInfo));
155
+ }, [routesInfo, modules]);
156
+
157
+ return (
158
+ <ConfigContext.Provider value={config}>
159
+ <PreloadProvider routes={routesInfo} modules={modules}>
160
+ <ScrollHandler />
161
+ <Routes>
162
+ {/* Custom home page WITHOUT docs layout */}
163
+ {HomePage && (
164
+ <Route
165
+ path="/"
166
+ element={
167
+ <ThemeLayout
168
+ config={config}
169
+ routes={routesInfo}
170
+ sidebar={null}
171
+ toc={null}
172
+ breadcrumbs={null}
173
+ {...config.themeConfig?.layoutProps}
174
+ >
175
+ <HomePage />
176
+ </ThemeLayout>
177
+ }
178
+ />
179
+ )}
180
+
181
+ {/* Documentation pages WITH sidebar + TOC layout */}
182
+ <Route
183
+ key="docs-layout"
184
+ element={<DocsLayout config={config} routes={routesInfo} />}
185
+ >
186
+ {resolvedRoutes.map((route: any) => (
187
+ <Route
188
+ key={route.path}
189
+ path={route.path === "" ? "/" : route.path}
190
+ element={
191
+ <React.Suspense fallback={<Loading />}>
192
+ <MdxPage
193
+ Component={route.Component}
194
+ customComponents={customComponents}
195
+ />
196
+ </React.Suspense>
197
+ }
198
+ />
199
+ ))}
200
+ </Route>
201
+
202
+ <Route
203
+ path="*"
204
+ element={
205
+ <ThemeLayout
206
+ config={config}
207
+ routes={routesInfo}
208
+ {...config.themeConfig?.layoutProps}
209
+ >
210
+ <NotFound />
211
+ </ThemeLayout>
212
+ }
213
+ />
214
+ </Routes>
215
+ </PreloadProvider>
216
+ </ConfigContext.Provider>
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Handles scroll restoration and hash scrolling on navigation.
222
+ */
223
+ function ScrollHandler() {
224
+ const { pathname, hash } = useLocation();
225
+
226
+ useLayoutEffect(() => {
227
+ // Only scroll if we are not in a pending transition state (if we were using useTransition)
228
+ // For now, we ensure the scroll happens.
229
+ if (hash) {
230
+ const id = hash.replace("#", "");
231
+ const element = document.getElementById(id);
232
+ if (element) {
233
+ const offset = 80;
234
+ const bodyRect = document.body.getBoundingClientRect().top;
235
+ const elementRect = element.getBoundingClientRect().top;
236
+ const elementPosition = elementRect - bodyRect;
237
+ const offsetPosition = elementPosition - offset;
238
+
239
+ window.scrollTo({
240
+ top: offsetPosition,
241
+ behavior: "smooth",
242
+ });
243
+ return;
244
+ }
245
+ }
246
+ window.scrollTo(0, 0);
247
+ }, [pathname, hash]);
248
+
249
+ return null;
250
+ }
251
+
252
+ /** Wrapper layout for doc pages (sidebar + content + TOC) */
253
+ function DocsLayout({
254
+ config,
255
+ routes,
256
+ }: {
257
+ config: any;
258
+ routes: ComponentRoute[];
259
+ }) {
260
+ return (
261
+ <ThemeLayout
262
+ config={config}
263
+ routes={routes}
264
+ {...config.themeConfig?.layoutProps}
265
+ >
266
+ <Outlet />
267
+ </ThemeLayout>
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Renders an MDX page securely, injecting required custom components.
273
+ * For example, this overrides the default `<pre>` HTML tags emitted by MDX
274
+ * with the Boltdocs `CodeBlock` component for syntax highlighting.
275
+ *
276
+ * @param props - Contains the dynamically loaded React component representing the MDX page
277
+ */
278
+ function MdxPage({
279
+ Component,
280
+ customComponents = {},
281
+ }: {
282
+ Component: React.LazyExoticComponent<any>;
283
+ customComponents?: Record<string, React.ComponentType<any>>;
284
+ }) {
285
+ const allComponents = { ...mdxComponents, ...customComponents };
286
+ return (
287
+ <MDXProvider components={allComponents}>
288
+ <Component />
289
+ </MDXProvider>
290
+ );
291
+ }
292
+
293
+ /**
294
+ * Creates and mounts the Boltdocs documentation app.
295
+ *
296
+ * Usage:
297
+ * ```tsx
298
+ * import { createBoltdocsApp } from 'boltdocs/client'
299
+ * import routes from 'virtual:boltdocs-routes'
300
+ * import config from 'virtual:boltdocs-config'
301
+ * import 'boltdocs/style.css'
302
+ * import HomePage from './HomePage'
303
+ *
304
+ * createBoltdocsApp({
305
+ * target: '#root',
306
+ * routes,
307
+ * config,
308
+ * modules: import.meta.glob('/docs/**\/*.{md,mdx}'),
309
+ * hot: import.meta.hot,
310
+ * homePage: HomePage,
311
+ * })
312
+ * ```
313
+ */
314
+ export function createBoltdocsApp(options: CreateBoltdocsAppOptions) {
315
+ const { target, routes, config, modules, hot, homePage } = options;
316
+ const container = document.querySelector(target);
317
+ if (!container) {
318
+ throw new Error(
319
+ `[boltdocs] Mount target "${target}" not found in document.`,
320
+ );
321
+ }
322
+
323
+ const app = (
324
+ <React.StrictMode>
325
+ <BrowserRouter>
326
+ <AppShell
327
+ initialRoutes={routes}
328
+ initialConfig={config}
329
+ modules={modules}
330
+ hot={hot}
331
+ homePage={homePage}
332
+ components={options.components}
333
+ />
334
+ </BrowserRouter>
335
+ </React.StrictMode>
336
+ );
337
+
338
+ // SSG pre-renders a shell with mock components for SEO crawlers.
339
+ // We always use createRoot because the SSG output doesn't match the
340
+ // real client-side component tree (components are lazy/dynamic).
341
+ // Clear any SSG placeholder content before mounting.
342
+ container.innerHTML = "";
343
+ ReactDOM.createRoot(container as HTMLElement).render(app);
344
+ }