boltdocs 2.1.1 → 2.3.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 (133) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/boltdocs.js +2 -2
  3. package/dist/base-ui/index.d.mts +25 -0
  4. package/dist/base-ui/index.d.ts +25 -0
  5. package/dist/base-ui/index.js +1 -0
  6. package/dist/base-ui/index.mjs +1 -0
  7. package/dist/{cache-Q4T6VAUL.mjs → cache-P6WK424C.mjs} +1 -1
  8. package/dist/chunk-22NXDNP4.mjs +74 -0
  9. package/dist/chunk-2HUVMMJU.mjs +1 -0
  10. package/dist/chunk-2Z5T6EAU.mjs +1 -0
  11. package/dist/chunk-CRZGOE32.mjs +1 -0
  12. package/dist/chunk-HA6543SL.mjs +1 -0
  13. package/dist/chunk-JD3RSDE4.mjs +1 -0
  14. package/dist/chunk-JZXLCA2E.mjs +1 -0
  15. package/dist/chunk-NBCYHLAA.mjs +1 -0
  16. package/dist/chunk-RPUERTVC.mjs +1 -0
  17. package/dist/chunk-T3W44KWY.mjs +1 -0
  18. package/dist/chunk-URTD6E6S.mjs +1 -0
  19. package/dist/chunk-W2NB4T6V.mjs +1 -0
  20. package/dist/chunk-Y4RRHPXC.mjs +1 -0
  21. package/dist/client/index.d.mts +13 -115
  22. package/dist/client/index.d.ts +13 -115
  23. package/dist/client/index.js +1 -1
  24. package/dist/client/index.mjs +1 -1
  25. package/dist/client/ssr.js +1 -1
  26. package/dist/client/ssr.mjs +1 -1
  27. package/dist/client/types.d.mts +3 -0
  28. package/dist/client/types.d.ts +3 -0
  29. package/dist/client/types.js +1 -0
  30. package/dist/client/types.mjs +0 -0
  31. package/dist/copy-markdown-C-90ixSe.d.ts +15 -0
  32. package/dist/copy-markdown-CbS8X-qe.d.mts +15 -0
  33. package/dist/{client/hooks → hooks}/index.d.mts +16 -11
  34. package/dist/{client/hooks → hooks}/index.d.ts +16 -11
  35. package/dist/hooks/index.js +1 -0
  36. package/dist/hooks/index.mjs +1 -0
  37. package/dist/integrations/index.d.mts +48 -0
  38. package/dist/integrations/index.d.ts +48 -0
  39. package/dist/integrations/index.js +1 -0
  40. package/dist/integrations/index.mjs +1 -0
  41. package/dist/link-DfBwCeZc.d.mts +68 -0
  42. package/dist/link-DfBwCeZc.d.ts +68 -0
  43. package/dist/loading-B7X5Wchs.d.ts +66 -0
  44. package/dist/loading-WuaQbsKb.d.mts +66 -0
  45. package/dist/{client/components/mdx → mdx}/index.d.mts +6 -38
  46. package/dist/{client/components/mdx → mdx}/index.d.ts +6 -38
  47. package/dist/mdx/index.js +1 -0
  48. package/dist/mdx/index.mjs +1 -0
  49. package/dist/node/cli-entry.js +31 -27
  50. package/dist/node/cli-entry.mjs +5 -1
  51. package/dist/node/index.d.mts +44 -14
  52. package/dist/node/index.d.ts +44 -14
  53. package/dist/node/index.js +24 -24
  54. package/dist/node/index.mjs +1 -1
  55. package/dist/primitives/index.d.mts +301 -0
  56. package/dist/primitives/index.d.ts +301 -0
  57. package/dist/primitives/index.js +1 -0
  58. package/dist/primitives/index.mjs +1 -0
  59. package/dist/search-dialog-ZRXBAQJ5.mjs +1 -0
  60. package/dist/{types-Cp21DHI6.d.mts → types-j7jvWsJj.d.mts} +63 -17
  61. package/dist/{types-Cp21DHI6.d.ts → types-j7jvWsJj.d.ts} +63 -17
  62. package/dist/{use-routes-xLhumjbV.d.ts → use-routes-Cd806kGw.d.ts} +1 -1
  63. package/dist/{use-routes-8Iei6jTp.d.mts → use-routes-DDL0_jkQ.d.mts} +1 -1
  64. package/package.json +35 -8
  65. package/src/client/app/index.tsx +155 -35
  66. package/src/client/app/mdx-component.tsx +7 -3
  67. package/src/client/app/theme-context.tsx +47 -23
  68. package/src/client/components/default-layout.tsx +16 -6
  69. package/src/client/components/primitives/breadcrumbs.tsx +1 -1
  70. package/src/client/components/primitives/navbar.tsx +8 -5
  71. package/src/client/components/primitives/search-dialog.tsx +15 -6
  72. package/src/client/components/primitives/sidebar.tsx +3 -2
  73. package/src/client/components/primitives/skeleton.tsx +26 -0
  74. package/src/client/components/ui-base/breadcrumbs.tsx +1 -1
  75. package/src/client/components/ui-base/index.ts +17 -0
  76. package/src/client/components/ui-base/loading.tsx +43 -73
  77. package/src/client/components/ui-base/navbar.tsx +74 -39
  78. package/src/client/components/ui-base/page-nav.tsx +2 -1
  79. package/src/client/components/ui-base/powered-by.tsx +11 -5
  80. package/src/client/components/ui-base/search-dialog.tsx +16 -5
  81. package/src/client/components/ui-base/sidebar.tsx +33 -22
  82. package/src/client/components/ui-base/tabs.tsx +4 -1
  83. package/src/client/components/ui-base/theme-toggle.tsx +35 -15
  84. package/src/client/hooks/use-i18n.ts +38 -7
  85. package/src/client/hooks/use-localized-to.ts +51 -73
  86. package/src/client/hooks/use-navbar.ts +10 -3
  87. package/src/client/hooks/use-page-nav.ts +27 -6
  88. package/src/client/hooks/use-routes.ts +62 -17
  89. package/src/client/hooks/use-search.ts +84 -46
  90. package/src/client/hooks/use-sidebar.ts +6 -2
  91. package/src/client/hooks/use-version.ts +5 -0
  92. package/src/client/integrations/index.ts +1 -0
  93. package/src/client/store/use-boltdocs-store.ts +44 -0
  94. package/src/client/theme/neutral.css +29 -0
  95. package/src/client/types.ts +4 -2
  96. package/src/client/utils/i18n.ts +23 -0
  97. package/src/node/{cli.ts → cli/build.ts} +17 -23
  98. package/src/node/cli/dev.ts +22 -0
  99. package/src/node/cli/doctor.ts +243 -0
  100. package/src/node/cli/index.ts +9 -0
  101. package/src/node/cli/ui.ts +54 -0
  102. package/src/node/cli-entry.ts +16 -16
  103. package/src/node/config.ts +54 -17
  104. package/src/node/index.ts +1 -1
  105. package/src/node/mdx/cache.ts +12 -0
  106. package/src/node/mdx/highlighter.ts +47 -0
  107. package/src/node/mdx/index.ts +114 -0
  108. package/src/node/mdx/rehype-shiki.ts +53 -0
  109. package/src/node/mdx/remark-shiki.ts +61 -0
  110. package/src/node/plugin/entry.ts +1 -1
  111. package/src/node/plugin/html.ts +8 -4
  112. package/src/node/plugin/index.ts +135 -72
  113. package/src/node/routes/index.ts +34 -13
  114. package/src/node/routes/parser.ts +13 -5
  115. package/src/node/search/index.ts +55 -0
  116. package/src/node/ssg/index.ts +15 -7
  117. package/src/node/ssg/robots.ts +7 -4
  118. package/src/node/utils.ts +32 -2
  119. package/tsup.config.ts +7 -2
  120. package/dist/chunk-52MVMZWS.mjs +0 -1
  121. package/dist/chunk-BVWWKXJH.mjs +0 -1
  122. package/dist/chunk-DVY3RDXD.mjs +0 -1
  123. package/dist/chunk-FUVYCYWC.mjs +0 -1
  124. package/dist/chunk-GBLMDJ2B.mjs +0 -1
  125. package/dist/chunk-ISPX45DF.mjs +0 -1
  126. package/dist/chunk-PNXZMUCO.mjs +0 -1
  127. package/dist/chunk-V2ZHKQSP.mjs +0 -74
  128. package/dist/client/components/mdx/index.js +0 -1
  129. package/dist/client/components/mdx/index.mjs +0 -1
  130. package/dist/client/hooks/index.js +0 -1
  131. package/dist/client/hooks/index.mjs +0 -1
  132. package/dist/search-dialog-TWGYKF2D.mjs +0 -1
  133. package/src/node/mdx.ts +0 -279
@@ -9,9 +9,9 @@ import type { BoltdocsPluginOptions } from './types'
9
9
  import { generateEntryCode } from './entry'
10
10
  import { injectHtmlMeta, getHtmlTemplate } from './html'
11
11
  import { generateRobotsTxt } from '../ssg/robots'
12
+ import { generateSearchData } from '../search'
12
13
  import fs from 'fs'
13
14
 
14
-
15
15
  export * from './types'
16
16
 
17
17
  /**
@@ -55,7 +55,20 @@ export function boltdocsPlugin(
55
55
  }
56
56
 
57
57
  return {
58
- optimizeDeps: { include: ['react', 'react-dom'] },
58
+ optimizeDeps: {
59
+ include: ['react', 'react-dom'],
60
+ exclude: [
61
+ 'boltdocs',
62
+ 'boltdocs/client',
63
+ 'boltdocs/hooks',
64
+ 'boltdocs/primitives',
65
+ 'boltdocs/base-ui',
66
+ 'boltdocs/mdx',
67
+ 'boltdocs/integrations',
68
+ 'boltdocs/client/hooks',
69
+ 'boltdocs/client/primitives',
70
+ ],
71
+ },
59
72
  }
60
73
  },
61
74
 
@@ -76,25 +89,39 @@ export function boltdocsPlugin(
76
89
  next()
77
90
  })
78
91
 
79
- // Serve default HTML if index.html is missing
92
+ // Serve default HTML for documentation routes or if index.html is missing
80
93
  server.middlewares.use(async (req, res, next) => {
81
94
  const url = req.url?.split('?')[0] || '/'
82
95
  const accept = req.headers.accept || ''
83
96
 
84
- if (
85
- accept.includes('text/html') &&
86
- !url.includes('.') // Simple check for assets
87
- ) {
88
- const indexPath = path.resolve(process.cwd(), 'index.html')
89
- if (!fs.existsSync(indexPath)) {
90
- let html = getHtmlTemplate(config)
91
- html = injectHtmlMeta(html, config)
92
- html = await server.transformIndexHtml(req.url || '/', html)
93
- res.statusCode = 200
94
- res.setHeader('Content-Type', 'text/html')
95
- res.end(html)
96
- return
97
- }
97
+ const isDocRoute =
98
+ url === '/' ||
99
+ url.startsWith('/docs') ||
100
+ (config.i18n &&
101
+ Object.keys(config.i18n.locales).some(
102
+ (locale) =>
103
+ url.startsWith(`/${locale}/docs`) || url === `/${locale}`,
104
+ )) ||
105
+ (config.external &&
106
+ Object.keys(config.external).some((extPath) =>
107
+ url.startsWith(extPath),
108
+ ))
109
+
110
+ // Improved check: If it's a doc route, serve HTML even if it has a dot (e.g. version 1.1)
111
+ // We only skip if it has a known asset extension to prevent serving HTML for images/js/etc.
112
+ const isAsset =
113
+ /\.(js|css|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|otf|mp4|webm|ogg|mp3|wav|flac|aac|pdf|zip|gz|map|json)$/i.test(
114
+ url,
115
+ )
116
+
117
+ if (accept.includes('text/html') && !isAsset && isDocRoute) {
118
+ let html = getHtmlTemplate(config)
119
+ html = injectHtmlMeta(html, config)
120
+ html = await server.transformIndexHtml(req.url || '/', html)
121
+ res.statusCode = 200
122
+ res.setHeader('Content-Type', 'text/html')
123
+ res.end(html)
124
+ return
98
125
  }
99
126
 
100
127
  next()
@@ -124,66 +151,96 @@ export function boltdocsPlugin(
124
151
  file: string,
125
152
  type: 'add' | 'unlink' | 'change',
126
153
  ) => {
127
- const normalized = normalizePath(file)
128
-
129
- // Restart the Vite server if the Boltdocs config changes
130
- if (CONFIG_FILES.some((c) => normalized.endsWith(c))) {
131
- server.restart()
132
- return
133
- }
154
+ try {
155
+ const normalized = normalizePath(file)
134
156
 
135
- // If mdx-components file changes, invalidate the virtual module
136
- if (
137
- mdxCompExtensions.some((ext) =>
138
- normalized.endsWith(`mdx-components.${ext}`),
139
- )
140
- ) {
141
- const mod = server.moduleGraph.getModuleById(
142
- '\0virtual:boltdocs-mdx-components',
143
- )
144
- if (mod) server.moduleGraph.invalidateModule(mod)
145
- server.ws.send({ type: 'full-reload' })
146
- return
147
- }
157
+ // Restart the Vite server if the Boltdocs config changes
158
+ if (CONFIG_FILES.some((c) => normalized.endsWith(c))) {
159
+ server.restart()
160
+ return
161
+ }
148
162
 
149
- // If layout.tsx/jsx file changes, invalidate the virtual module
150
- if (
151
- compExtensions.some((ext) => normalized.endsWith(`layout.${ext}`))
152
- ) {
153
- const mod = server.moduleGraph.getModuleById(
154
- '\0virtual:boltdocs-layout',
155
- )
156
- if (mod) server.moduleGraph.invalidateModule(mod)
157
- server.ws.send({ type: 'full-reload' })
158
- return
159
- }
163
+ // If mdx-components file changes, invalidate the virtual module
164
+ if (
165
+ mdxCompExtensions.some((ext) =>
166
+ normalized.endsWith(`mdx-components.${ext}`),
167
+ )
168
+ ) {
169
+ const mod = server.moduleGraph.getModuleById(
170
+ '\0virtual:boltdocs-mdx-components',
171
+ )
172
+ if (mod) server.moduleGraph.invalidateModule(mod)
173
+ server.ws.send({ type: 'full-reload' })
174
+ return
175
+ }
160
176
 
161
- if (
162
- !normalized.startsWith(normalizedDocsDir) ||
163
- !isDocFile(normalized)
164
- )
165
- return
177
+ // If layout.tsx/jsx file changes, invalidate the virtual module
178
+ if (
179
+ compExtensions.some((ext) => normalized.endsWith(`layout.${ext}`))
180
+ ) {
181
+ const mod = server.moduleGraph.getModuleById(
182
+ '\0virtual:boltdocs-layout',
183
+ )
184
+ if (mod) server.moduleGraph.invalidateModule(mod)
185
+ server.ws.send({ type: 'full-reload' })
186
+ return
187
+ }
166
188
 
167
- // Invalidate appropriately
168
- if (type === 'add' || type === 'unlink') {
169
- invalidateRouteCache()
170
- } else {
171
- invalidateFile(file)
172
- }
189
+ if (
190
+ !normalized.startsWith(normalizedDocsDir) ||
191
+ !isDocFile(normalized)
192
+ )
193
+ return
173
194
 
174
- // Regenerate and push to client
175
- const newRoutes = await generateRoutes(docsDir, config)
195
+ // Invalidate appropriately
196
+ if (type === 'add' || type === 'unlink') {
197
+ invalidateRouteCache()
198
+ // Re-resolve config as it might affect versions/routes
199
+ config = await resolveConfig(docsDir)
200
+
201
+ const configMod = server.moduleGraph.getModuleById(
202
+ '\0virtual:boltdocs-config',
203
+ )
204
+ if (configMod) server.moduleGraph.invalidateModule(configMod)
205
+
206
+ server.ws.send({
207
+ type: 'custom',
208
+ event: 'boltdocs:config-update',
209
+ data: {
210
+ theme: config?.theme,
211
+ integrations: config?.integrations,
212
+ i18n: config?.i18n,
213
+ versions: config?.versions,
214
+ siteUrl: config?.siteUrl,
215
+ },
216
+ })
217
+ } else {
218
+ invalidateFile(file)
219
+ }
176
220
 
177
- const routesMod = server.moduleGraph.getModuleById(
178
- '\0virtual:boltdocs-routes',
179
- )
180
- if (routesMod) server.moduleGraph.invalidateModule(routesMod)
221
+ // Regenerate and push to client
222
+ // Optimization: generateRoutes is mostly incremental thanks to docCache
223
+ // We only force a full disk scan on add/unlink events
224
+ const newRoutes = await generateRoutes(
225
+ docsDir,
226
+ config,
227
+ '/docs',
228
+ type !== 'change',
229
+ )
181
230
 
182
- server.ws.send({
183
- type: 'custom',
184
- event: 'boltdocs:routes-update',
185
- data: newRoutes,
186
- })
231
+ const routesMod = server.moduleGraph.getModuleById(
232
+ '\0virtual:boltdocs-routes',
233
+ )
234
+ if (routesMod) server.moduleGraph.invalidateModule(routesMod)
235
+
236
+ server.ws.send({
237
+ type: 'custom',
238
+ event: 'boltdocs:routes-update',
239
+ data: newRoutes,
240
+ })
241
+ } catch (e) {
242
+ console.error(`[boltdocs] HMR error during ${type} event:`, e)
243
+ }
187
244
  }
188
245
 
189
246
  server.watcher.on('add', (f) => handleFileEvent(f, 'add'))
@@ -197,7 +254,8 @@ export function boltdocsPlugin(
197
254
  id === 'virtual:boltdocs-config' ||
198
255
  id === 'virtual:boltdocs-entry' ||
199
256
  id === 'virtual:boltdocs-mdx-components' ||
200
- id === 'virtual:boltdocs-layout'
257
+ id === 'virtual:boltdocs-layout' ||
258
+ id === 'virtual:boltdocs-search'
201
259
  ) {
202
260
  return '\0' + id
203
261
  }
@@ -211,7 +269,6 @@ export function boltdocsPlugin(
211
269
  if (id === '\0virtual:boltdocs-config') {
212
270
  const clientConfig = {
213
271
  theme: config?.theme,
214
- themeConfig: config?.themeConfig,
215
272
  integrations: config?.integrations,
216
273
  i18n: config?.i18n,
217
274
  versions: config?.versions,
@@ -267,6 +324,12 @@ export default UserLayout;`
267
324
  return `import { DefaultLayout } from 'boltdocs/client';
268
325
  export default DefaultLayout;`
269
326
  }
327
+
328
+ if (id === '\0virtual:boltdocs-search') {
329
+ const routes = await generateRoutes(docsDir, config)
330
+ const searchData = generateSearchData(routes)
331
+ return `export default ${JSON.stringify(searchData, null, 2)};`
332
+ }
270
333
  },
271
334
 
272
335
  transformIndexHtml: {
@@ -11,7 +11,8 @@ import { sortRoutes } from './sorter'
11
11
  export type { RouteMeta }
12
12
  export { invalidateRouteCache, invalidateFile }
13
13
 
14
- // Cache for localized path computations
14
+ // Cache for file list and localized path computations
15
+ let cachedFileList: string[] | null = null
15
16
  const localizedPathCache = new Map<string, string>()
16
17
 
17
18
  /**
@@ -30,6 +31,7 @@ export async function generateRoutes(
30
31
  docsDir: string,
31
32
  config?: BoltdocsConfig,
32
33
  basePath: string = '/docs',
34
+ forceScan: boolean = true,
33
35
  ): Promise<RouteMeta[]> {
34
36
  const start = performance.now()
35
37
 
@@ -44,13 +46,19 @@ export async function generateRoutes(
44
46
  docCache.invalidateAll()
45
47
  }
46
48
 
47
- // 1. FAST SCAN
48
- const files = await fastGlob(['**/*.md', '**/*.mdx'], {
49
- cwd: docsDir,
50
- absolute: true,
51
- suppressErrors: true,
52
- followSymbolicLinks: false,
53
- })
49
+ // 1. FAST SCAN (Skip if incremental and we have a cache)
50
+ let files: string[]
51
+ if (!forceScan && cachedFileList) {
52
+ files = cachedFileList
53
+ } else {
54
+ files = await fastGlob(['**/*.md', '**/*.mdx'], {
55
+ cwd: docsDir,
56
+ absolute: true,
57
+ suppressErrors: true,
58
+ followSymbolicLinks: false,
59
+ })
60
+ cachedFileList = files
61
+ }
54
62
 
55
63
  // Prune cache entries for deleted files
56
64
  docCache.pruneStale(new Set(files))
@@ -197,8 +205,6 @@ function generateI18nFallbacks(
197
205
  }
198
206
 
199
207
  for (const locale of allLocales) {
200
- if (locale === defaultLocale) continue
201
-
202
208
  const localePaths = routesByLocale.get(locale) || new Set<string>()
203
209
 
204
210
  for (const defRoute of defaultRoutes) {
@@ -207,8 +213,12 @@ function generateI18nFallbacks(
207
213
  defaultLocale,
208
214
  locale,
209
215
  basePath,
216
+ config,
210
217
  )
211
218
 
219
+ // Skip if the path is already the same (e.g. for default locale unprefixed)
220
+ if (targetPath === defRoute.path) continue
221
+
212
222
  if (!localePaths.has(targetPath)) {
213
223
  fallbackRoutes.push({
214
224
  ...defRoute,
@@ -231,15 +241,26 @@ function computeLocalizedPath(
231
241
  defaultLocale: string,
232
242
  targetLocale: string,
233
243
  basePath: string,
244
+ config?: BoltdocsConfig,
234
245
  ): string {
235
246
  const cacheKey = `${path}:${targetLocale}`
236
247
  const cached = localizedPathCache.get(cacheKey)
237
248
  if (cached) return cached
238
249
 
239
250
  let prefix = basePath
240
- const versionMatch = path.match(new RegExp(`^${basePath}/(v[0-9]+)`))
241
- if (versionMatch) {
242
- prefix += '/' + versionMatch[1]
251
+ if (config?.versions) {
252
+ const vPrefix = config.versions.prefix || ''
253
+ for (const vConfig of config.versions.versions) {
254
+ const fullVPath = vPrefix + vConfig.path
255
+ if (path.startsWith(`${basePath}/${fullVPath}`)) {
256
+ prefix += '/' + fullVPath
257
+ break
258
+ }
259
+ if (path.startsWith(`${basePath}/${vConfig.path}`)) {
260
+ prefix += '/' + vConfig.path
261
+ break
262
+ }
263
+ }
243
264
  }
244
265
 
245
266
  let pathAfterVersion = path.substring(prefix.length)
@@ -59,8 +59,15 @@ export function parseDocFile(
59
59
  // Level 1: Check for version
60
60
  if (config?.versions && parts.length > 0) {
61
61
  const potentialVersion = parts[0]
62
- if (config.versions.versions[potentialVersion]) {
63
- version = potentialVersion
62
+ const prefix = config.versions.prefix || ''
63
+
64
+ const versionMatch = config.versions.versions.find((v) => {
65
+ const fullPath = prefix + v.path
66
+ return potentialVersion === fullPath || potentialVersion === v.path
67
+ })
68
+
69
+ if (versionMatch) {
70
+ version = versionMatch.path
64
71
  parts = parts.slice(1)
65
72
  }
66
73
  }
@@ -124,8 +131,7 @@ export function parseDocFile(
124
131
  const headings: { level: number; text: string; id: string }[] = []
125
132
  const headingsRegex = /^(#{2,4})\s+(.+)$/gm
126
133
 
127
- let match
128
- while ((match = headingsRegex.exec(content)) !== null) {
134
+ for (const match of content.matchAll(headingsRegex)) {
129
135
  const level = match[1].length
130
136
  const rawText = match[2]
131
137
  .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Strip markdown links
@@ -160,7 +166,9 @@ export function parseDocFile(
160
166
  sanitizedDescription = plainExcerpt
161
167
  }
162
168
 
163
- const sanitizedBadge = data.badge ? sanitizeHtml(String(data.badge)) : undefined
169
+ const sanitizedBadge = data.badge
170
+ ? sanitizeHtml(String(data.badge))
171
+ : undefined
164
172
  const icon = data.icon ? String(data.icon) : undefined
165
173
 
166
174
  // Extract full content as plain text for search indexing
@@ -0,0 +1,55 @@
1
+ import type { RouteMeta } from '../routes/types'
2
+
3
+ export interface SearchDocument {
4
+ id: string
5
+ title: string
6
+ content: string
7
+ url: string
8
+ display: string
9
+ locale?: string
10
+ version?: string
11
+ }
12
+
13
+ /**
14
+ * Generates a flat list of searchable documents from the route metadata.
15
+ * Each page is indexed as a primary document, and its sections (headings)
16
+ * are indexed as secondary documents to provide granular search results.
17
+ */
18
+ export function generateSearchData(routes: RouteMeta[]): SearchDocument[] {
19
+ const documents: SearchDocument[] = []
20
+
21
+ for (const route of routes) {
22
+ // 1. Index the main page
23
+ documents.push({
24
+ id: route.path,
25
+ title: route.title,
26
+ content: route._content || '',
27
+ url: route.path,
28
+ display: route.groupTitle
29
+ ? `${route.groupTitle} > ${route.title}`
30
+ : route.title,
31
+ locale: route.locale,
32
+ version: route.version,
33
+ })
34
+
35
+ // 2. Index headings as sub-documents for deep linking
36
+ if (route.headings) {
37
+ for (const heading of route.headings) {
38
+ // We find the content belonging to this heading?
39
+ // For now, indexing just the heading text and a bit of context is standard.
40
+ // Deep full-text mapping to specific headings is more complex.
41
+ documents.push({
42
+ id: `${route.path}#${heading.id}`,
43
+ title: heading.text,
44
+ content: `${heading.text} in ${route.title}`,
45
+ url: `${route.path}#${heading.id}`,
46
+ display: `${route.title} > ${heading.text}`,
47
+ locale: route.locale,
48
+ version: route.version,
49
+ })
50
+ }
51
+ }
52
+ }
53
+
54
+ return documents
55
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { generateRoutes } from '../routes'
4
- import { escapeHtml } from '../utils'
4
+ import { escapeHtml, getTranslated } from '../utils'
5
5
  import { fileURLToPath } from 'url'
6
6
  import { createRequire } from 'module'
7
7
 
@@ -27,8 +27,6 @@ const _require = createRequire(import.meta.url)
27
27
  export async function generateStaticPages(options: SSGOptions): Promise<void> {
28
28
  const { docsDir, docsDirName, outDir, config } = options
29
29
  const routes = await generateRoutes(docsDir, config)
30
- const siteTitle = config?.themeConfig?.title || 'Boltdocs'
31
- const siteDescription = config?.themeConfig?.description || ''
32
30
 
33
31
  // Resolve the SSR module (compiled by tsup)
34
32
  const ssrModulePath = path.resolve(_dirname, '../client/ssr.js')
@@ -50,14 +48,17 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
50
48
  __esModule: true,
51
49
  default: function SSG_Virtual_Layout(props: any) {
52
50
  try {
53
- const client = originalRequire.apply(this, [path.resolve(_dirname, '../client/index.js')])
54
- const Comp = client.DefaultLayout || (({ children }: any) => children)
51
+ const client = originalRequire.apply(this, [
52
+ path.resolve(_dirname, '../client/index.js'),
53
+ ])
54
+ const Comp =
55
+ client.DefaultLayout || (({ children }: any) => children)
55
56
  const React = originalRequire.apply(this, ['react'])
56
57
  return React.createElement(Comp, props)
57
58
  } catch (e) {
58
59
  return props.children
59
60
  }
60
- }
61
+ },
61
62
  }
62
63
  }
63
64
  return originalRequire.apply(this, [id, ...args])
@@ -79,6 +80,10 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
79
80
  // Generate an HTML file for each route concurrently
80
81
  await Promise.all(
81
82
  routes.map(async (route) => {
83
+ const siteTitle =
84
+ getTranslated(config?.theme?.title, route.locale) || 'Boltdocs'
85
+ const siteDescription =
86
+ getTranslated(config?.theme?.description, route.locale) || ''
82
87
  const pageTitle = `${route.title} | ${siteTitle}`
83
88
  const pageDescription = route.description || siteDescription
84
89
 
@@ -111,7 +116,10 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
111
116
  'utf-8',
112
117
  )
113
118
  } catch (e: any) {
114
- console.error(`[boltdocs] Error SSR rendering route ${route.path}:`, e ? e.stack || e : e)
119
+ console.error(
120
+ `[boltdocs] Error SSR rendering route ${route.path}:`,
121
+ e ? e.stack || e : e,
122
+ )
115
123
  }
116
124
  }),
117
125
  )
@@ -19,20 +19,23 @@ export function generateRobotsTxt(config: BoltdocsConfig): string {
19
19
  allow: '/',
20
20
  },
21
21
  ]
22
- const sitemaps = (robots as any).sitemaps || (siteUrl ? [`${siteUrl}/sitemap.xml`] : [])
22
+ const sitemaps =
23
+ (robots as any).sitemaps || (siteUrl ? [`${siteUrl}/sitemap.xml`] : [])
23
24
 
24
25
  let content = ''
25
26
 
26
27
  for (const rule of rules) {
27
28
  content += `User-agent: ${rule.userAgent}\n`
28
-
29
+
29
30
  if (rule.disallow) {
30
- const disallows = Array.isArray(rule.disallow) ? rule.disallow : [rule.disallow]
31
+ const disallows = Array.isArray(rule.disallow)
32
+ ? rule.disallow
33
+ : [rule.disallow]
31
34
  for (const d of disallows) {
32
35
  content += `Disallow: ${d}\n`
33
36
  }
34
37
  }
35
-
38
+
36
39
  if (rule.allow) {
37
40
  const allows = Array.isArray(rule.allow) ? rule.allow : [rule.allow]
38
41
  for (const a of allows) {
package/src/node/utils.ts CHANGED
@@ -72,8 +72,13 @@ export function parseFrontmatter(filePath: string): {
72
72
  content: string
73
73
  } {
74
74
  const raw = fs.readFileSync(filePath, 'utf-8')
75
- const { data, content } = matter(raw)
76
- return { data, content }
75
+ try {
76
+ const { data, content } = matter(raw)
77
+ return { data, content }
78
+ } catch (e) {
79
+ // If frontmatter is malformed (e.g. while editing), return empty data and raw content
80
+ return { data: {}, content: raw }
81
+ }
77
82
  }
78
83
 
79
84
  /**
@@ -169,3 +174,28 @@ export function stripHtmlTags(html: string): string {
169
174
  export function capitalize(str: string): string {
170
175
  return str.charAt(0).toUpperCase() + str.slice(1)
171
176
  }
177
+
178
+ /**
179
+ * Retrieves the correct translation from a value that can be either
180
+ * a simple string or a map of locale-specific strings.
181
+ * Node-side version for SSG and config resolution.
182
+ *
183
+ * @param value - The text to translate
184
+ * @param locale - The current active locale (e.g., 'en', 'es')
185
+ * @returns The translated string
186
+ */
187
+ export function getTranslated(
188
+ value: string | Record<string, string> | undefined,
189
+ locale?: string,
190
+ ): string {
191
+ if (!value) return ''
192
+ if (typeof value === 'string') return value
193
+
194
+ if (locale && value[locale]) {
195
+ return value[locale]
196
+ }
197
+
198
+ // Fallback: Use the first available translation or an empty string
199
+ const firstValue = Object.values(value)[0]
200
+ return firstValue || ''
201
+ }
package/tsup.config.ts CHANGED
@@ -20,6 +20,7 @@ const commonConfig: Options = {
20
20
  'virtual:boltdocs-layout',
21
21
  'virtual:boltdocs-mdx-components',
22
22
  'virtual:boltdocs-entry',
23
+ /^virtual:boltdocs-/, // Broad catch-all for any virtual modules
23
24
  ],
24
25
  }
25
26
 
@@ -41,8 +42,12 @@ export default defineConfig([
41
42
  platform: 'browser',
42
43
  entry: {
43
44
  'client/index': 'src/client/index.ts',
44
- 'client/hooks/index': 'src/client/hooks/index.ts',
45
- 'client/components/mdx/index': 'src/client/components/mdx/index.ts',
45
+ 'client/types': 'src/client/types.ts',
46
+ 'hooks/index': 'src/client/hooks/index.ts',
47
+ 'primitives/index': 'src/client/components/primitives/index.ts',
48
+ 'base-ui/index': 'src/client/components/ui-base/index.ts',
49
+ 'mdx/index': 'src/client/components/mdx/index.ts',
50
+ 'integrations/index': 'src/client/integrations/index.ts',
46
51
  },
47
52
  },
48
53
  // SSR Build (Needs Node platform for Server-Side Rendering)
@@ -1 +0,0 @@
1
- var v=Object.defineProperty;var S=(t,i)=>{for(var e in i)v(t,e,{get:i[e],enumerable:!0})};import{createContext as x,use as b}from"react";var y=x(null);function P(){let t=b(y);if(!t)throw new Error("useConfig must be used within a ConfigProvider");return t}import{useLocation as R}from"react-router-dom";function z(t){let i=R(),e=P();if(!e||typeof t!="string"||!e.i18n&&!e.versions)return t;let n="/docs";if(!t.startsWith(n))return t;let r=i.pathname.substring(n.length).split("/").filter(Boolean),u=e.versions?.defaultVersion,c=e.i18n?.defaultLocale,o=0;e.versions&&r.length>o&&e.versions.versions[r[o]]&&(u=r[o],o++),e.i18n&&r.length>o&&e.i18n.locales[r[o]]&&(c=r[o]);let s=t.substring(n.length).split("/").filter(Boolean),l=0,d=!1,m=!1;e.versions&&s.length>l&&e.versions.versions[s[l]]&&(d=!0,l++),e.i18n&&s.length>l&&e.i18n.locales[s[l]]&&(m=!0,l++);let C=s.slice(l),a=[];e.versions&&(d?a.push(s[0]):u&&a.push(u)),e.i18n&&(m?a.push(s[d?1:0]):c&&a.push(c)),a.push(...C);let f=`${n}/${a.join("/")}`;return f.endsWith("/")&&(f=f.slice(0,-1)),f===n?n:f}import{createContext as L,use as T,useCallback as k,useRef as w}from"react";import{jsx as B}from"react/jsx-runtime";var g=L({preload:()=>{},routes:[]});function K(){return T(g)}function N({routes:t,modules:i,children:e}){let n=w(null),h=k(r=>{n.current&&clearTimeout(n.current),n.current=setTimeout(()=>{let u=r.split("#")[0].split("?")[0],c=t.find(o=>o.path===u||u==="/"&&o.path==="");if(c?.filePath){let o=Object.keys(i).find(p=>p.endsWith("/"+c.filePath));o&&i[o]().catch(p=>{console.error(`[boltdocs] Failed to preload route ${r}:`,p)})}},100)},[t,i]);return B(g.Provider,{value:{preload:h,routes:t},children:e})}export{S as a,y as b,P as c,z as d,K as e,N as f};