boltdocs 1.3.0 → 1.3.1

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 (101) hide show
  1. package/dist/node/index.js +21 -6
  2. package/dist/node/index.mjs +21 -6
  3. package/package.json +1 -1
  4. package/src/client/app/index.tsx +344 -344
  5. package/src/client/app/preload.tsx +56 -56
  6. package/src/client/index.ts +40 -40
  7. package/src/client/ssr.tsx +51 -51
  8. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +76 -76
  9. package/src/client/theme/components/CodeBlock/index.ts +1 -1
  10. package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +154 -154
  11. package/src/client/theme/components/PackageManagerTabs/index.ts +1 -1
  12. package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +64 -64
  13. package/src/client/theme/components/Playground/Playground.tsx +124 -124
  14. package/src/client/theme/components/Playground/index.ts +1 -1
  15. package/src/client/theme/components/Playground/playground.css +168 -168
  16. package/src/client/theme/components/Video/Video.tsx +84 -84
  17. package/src/client/theme/components/Video/index.ts +1 -1
  18. package/src/client/theme/components/Video/video.css +41 -41
  19. package/src/client/theme/components/mdx/Admonition.tsx +80 -80
  20. package/src/client/theme/components/mdx/Badge.tsx +31 -31
  21. package/src/client/theme/components/mdx/Button.tsx +50 -50
  22. package/src/client/theme/components/mdx/Card.tsx +80 -80
  23. package/src/client/theme/components/mdx/List.tsx +57 -57
  24. package/src/client/theme/components/mdx/Tabs.tsx +94 -94
  25. package/src/client/theme/components/mdx/index.ts +18 -18
  26. package/src/client/theme/components/mdx/mdx-components.css +424 -424
  27. package/src/client/theme/icons/bun.tsx +62 -62
  28. package/src/client/theme/icons/deno.tsx +20 -20
  29. package/src/client/theme/icons/discord.tsx +12 -12
  30. package/src/client/theme/icons/github.tsx +15 -15
  31. package/src/client/theme/icons/npm.tsx +13 -13
  32. package/src/client/theme/icons/pnpm.tsx +72 -72
  33. package/src/client/theme/icons/twitter.tsx +12 -12
  34. package/src/client/theme/styles/markdown.css +343 -343
  35. package/src/client/theme/styles/variables.css +162 -162
  36. package/src/client/theme/styles.css +37 -37
  37. package/src/client/theme/ui/BackgroundGradient/BackgroundGradient.tsx +10 -10
  38. package/src/client/theme/ui/BackgroundGradient/index.ts +1 -1
  39. package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +68 -68
  40. package/src/client/theme/ui/Breadcrumbs/index.ts +1 -1
  41. package/src/client/theme/ui/Footer/footer.css +32 -32
  42. package/src/client/theme/ui/Head/Head.tsx +69 -69
  43. package/src/client/theme/ui/Head/index.ts +1 -1
  44. package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +125 -125
  45. package/src/client/theme/ui/LanguageSwitcher/index.ts +1 -1
  46. package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +98 -98
  47. package/src/client/theme/ui/Layout/Layout.tsx +202 -202
  48. package/src/client/theme/ui/Layout/base.css +76 -76
  49. package/src/client/theme/ui/Layout/index.ts +2 -2
  50. package/src/client/theme/ui/Layout/pagination.css +72 -72
  51. package/src/client/theme/ui/Layout/responsive.css +36 -36
  52. package/src/client/theme/ui/Link/Link.tsx +254 -254
  53. package/src/client/theme/ui/Link/index.ts +2 -2
  54. package/src/client/theme/ui/Loading/Loading.tsx +10 -10
  55. package/src/client/theme/ui/Loading/index.ts +1 -1
  56. package/src/client/theme/ui/Loading/loading.css +30 -30
  57. package/src/client/theme/ui/Navbar/GithubStars.tsx +27 -27
  58. package/src/client/theme/ui/Navbar/Navbar.tsx +145 -145
  59. package/src/client/theme/ui/Navbar/index.ts +2 -2
  60. package/src/client/theme/ui/Navbar/navbar.css +233 -233
  61. package/src/client/theme/ui/NotFound/NotFound.tsx +19 -19
  62. package/src/client/theme/ui/NotFound/index.ts +1 -1
  63. package/src/client/theme/ui/NotFound/not-found.css +64 -64
  64. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +235 -235
  65. package/src/client/theme/ui/OnThisPage/index.ts +1 -1
  66. package/src/client/theme/ui/OnThisPage/toc.css +132 -132
  67. package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +18 -18
  68. package/src/client/theme/ui/PoweredBy/index.ts +1 -1
  69. package/src/client/theme/ui/PoweredBy/powered-by.css +76 -76
  70. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +199 -199
  71. package/src/client/theme/ui/SearchDialog/index.ts +1 -1
  72. package/src/client/theme/ui/SearchDialog/search.css +152 -152
  73. package/src/client/theme/ui/Sidebar/Sidebar.tsx +204 -204
  74. package/src/client/theme/ui/Sidebar/index.ts +1 -1
  75. package/src/client/theme/ui/Sidebar/sidebar.css +236 -236
  76. package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +69 -69
  77. package/src/client/theme/ui/ThemeToggle/index.ts +1 -1
  78. package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +136 -136
  79. package/src/client/theme/ui/VersionSwitcher/index.ts +1 -1
  80. package/src/client/types.ts +50 -50
  81. package/src/client/utils.ts +26 -26
  82. package/src/node/cache.ts +408 -408
  83. package/src/node/config.ts +192 -192
  84. package/src/node/index.ts +21 -21
  85. package/src/node/mdx.ts +120 -120
  86. package/src/node/plugin/entry.ts +58 -58
  87. package/src/node/plugin/html.ts +55 -55
  88. package/src/node/plugin/index.ts +193 -193
  89. package/src/node/plugin/types.ts +11 -11
  90. package/src/node/routes/cache.ts +28 -28
  91. package/src/node/routes/index.ts +167 -167
  92. package/src/node/routes/parser.ts +153 -127
  93. package/src/node/routes/sorter.ts +42 -42
  94. package/src/node/routes/types.ts +49 -49
  95. package/src/node/ssg/index.ts +114 -114
  96. package/src/node/ssg/meta.ts +34 -34
  97. package/src/node/ssg/options.ts +13 -13
  98. package/src/node/ssg/sitemap.ts +54 -54
  99. package/src/node/utils.ts +134 -134
  100. package/tsconfig.json +20 -20
  101. 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
+ }