@raystack/chronicle 0.12.4 → 0.12.5

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 (36) hide show
  1. package/dist/cli/index.js +1763 -252
  2. package/package.json +8 -2
  3. package/src/cli/commands/build.ts +36 -7
  4. package/src/cli/commands/static-generate.ts +1071 -0
  5. package/src/cli/utils/mdx-error-report.ts +48 -0
  6. package/src/components/api/playground-dialog.tsx +65 -14
  7. package/src/components/mdx/code.tsx +8 -3
  8. package/src/components/mdx/image.tsx +15 -1
  9. package/src/components/mdx/index.tsx +10 -3
  10. package/src/components/ui/search.tsx +226 -64
  11. package/src/lib/data-urls.ts +23 -0
  12. package/src/lib/mdx-component-names.ts +15 -0
  13. package/src/lib/mdx-error.test.ts +78 -0
  14. package/src/lib/mdx-error.ts +91 -0
  15. package/src/lib/openapi.ts +1 -1
  16. package/src/lib/page-context.tsx +29 -14
  17. package/src/lib/preload.ts +4 -2
  18. package/src/lib/remark-validate-mdx.test.ts +95 -0
  19. package/src/lib/remark-validate-mdx.ts +85 -0
  20. package/src/lib/route-resolver.ts +7 -0
  21. package/src/lib/static-mode.ts +3 -0
  22. package/src/pages/DocsPage.tsx +3 -2
  23. package/src/pages/NotFound.module.css +10 -0
  24. package/src/pages/RenderError.tsx +18 -0
  25. package/src/server/App.tsx +16 -12
  26. package/src/server/api/apis-proxy.ts +32 -7
  27. package/src/server/dev-error-page.ts +133 -0
  28. package/src/server/entry-server.tsx +6 -0
  29. package/src/server/entry-static.tsx +151 -0
  30. package/src/server/error.ts +13 -1
  31. package/src/server/plugins/telemetry.ts +3 -2
  32. package/src/server/vite-config.ts +22 -9
  33. package/src/themes/default/Page.module.css +2 -0
  34. package/src/themes/paper/Layout.module.css +0 -4
  35. package/src/themes/paper/Layout.tsx +0 -2
  36. package/src/themes/paper/Page.module.css +2 -0
@@ -1,7 +1,11 @@
1
1
  import { defineHandler, HTTPError } from 'nitro';
2
+ import { StatusCodes } from 'http-status-codes';
2
3
  import { loadConfig } from '@/lib/config';
3
4
  import { loadApiSpecs } from '@/lib/openapi';
4
5
 
6
+ const MAX_BODY_SIZE = 10_485_760; // 10 MB
7
+ const UPSTREAM_TIMEOUT_MS = 120_000;
8
+
5
9
  interface ProxyRequest {
6
10
  specName: string;
7
11
  method: string;
@@ -10,9 +14,26 @@ interface ProxyRequest {
10
14
  body?: unknown;
11
15
  }
12
16
 
17
+ function isPathSafe(p: string): boolean {
18
+ let decoded: string;
19
+ try {
20
+ decoded = decodeURIComponent(p);
21
+ } catch {
22
+ return false;
23
+ }
24
+ if (/^[a-z]+:\/\//i.test(decoded)) return false;
25
+ const normalized = new URL(decoded, 'http://localhost').pathname;
26
+ return !normalized.split('/').includes('..');
27
+ }
28
+
13
29
  export default defineHandler(async event => {
14
30
  if (event.req.method !== 'POST') {
15
- throw new HTTPError({ status: 405, message: 'Method not allowed' });
31
+ throw new HTTPError({ status: StatusCodes.METHOD_NOT_ALLOWED, message: 'Method not allowed' });
32
+ }
33
+
34
+ const contentLength = parseInt(event.req.headers.get('content-length') ?? '0', 10);
35
+ if (contentLength > MAX_BODY_SIZE) {
36
+ throw new HTTPError({ status: StatusCodes.CONTENT_TOO_LARGE, message: `Request body too large (max ${MAX_BODY_SIZE} bytes)` });
16
37
  }
17
38
 
18
39
  const { specName, method, path, headers, body } =
@@ -20,7 +41,7 @@ export default defineHandler(async event => {
20
41
 
21
42
  if (!specName || !method || !path) {
22
43
  throw new HTTPError({
23
- status: 400,
44
+ status: StatusCodes.BAD_REQUEST,
24
45
  message: 'Missing specName, method, or path'
25
46
  });
26
47
  }
@@ -30,11 +51,11 @@ export default defineHandler(async event => {
30
51
  const spec = specs.find(s => s.name === specName);
31
52
 
32
53
  if (!spec) {
33
- throw new HTTPError({ status: 404, message: `Unknown spec: ${specName}` });
54
+ throw new HTTPError({ status: StatusCodes.NOT_FOUND, message: `Unknown spec: ${specName}` });
34
55
  }
35
56
 
36
- if (/^[a-z]+:\/\//i.test(path) || path.includes('..')) {
37
- throw new HTTPError({ status: 400, message: 'Invalid path' });
57
+ if (!isPathSafe(path)) {
58
+ throw new HTTPError({ status: StatusCodes.BAD_REQUEST, message: 'Invalid path' });
38
59
  }
39
60
 
40
61
  const url = spec.server.url + path;
@@ -43,7 +64,8 @@ export default defineHandler(async event => {
43
64
  const response = await fetch(url, {
44
65
  method,
45
66
  headers,
46
- body: body ? JSON.stringify(body) : undefined
67
+ body: body ? JSON.stringify(body) : undefined,
68
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
47
69
  });
48
70
 
49
71
  const contentType = response.headers.get('content-type') ?? '';
@@ -64,12 +86,15 @@ export default defineHandler(async event => {
64
86
  headers: responseHeaders
65
87
  });
66
88
  } catch (error) {
89
+ if (error instanceof DOMException && error.name === 'TimeoutError') {
90
+ throw new HTTPError({ status: StatusCodes.GATEWAY_TIMEOUT, message: `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS}ms` });
91
+ }
67
92
  const message =
68
93
  error instanceof Error
69
94
  ? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ''}`
70
95
  : 'Request failed';
71
96
  throw new HTTPError({
72
- status: 502,
97
+ status: StatusCodes.BAD_GATEWAY,
73
98
  message: `Could not reach ${url}\n${message}`
74
99
  });
75
100
  }
@@ -0,0 +1,133 @@
1
+ import { buildCodeFrame, type CodeFrame, parseMdxError } from '@/lib/mdx-error'
2
+
3
+ function escapeHtml(s: string): string {
4
+ return s
5
+ .replace(/&/g, '&')
6
+ .replace(/</g, '&lt;')
7
+ .replace(/>/g, '&gt;')
8
+ .replace(/"/g, '&quot;')
9
+ }
10
+
11
+ function displayPath(file: string): string {
12
+ const roots = [
13
+ typeof __CHRONICLE_CONTENT_DIR__ !== 'undefined' ? __CHRONICLE_CONTENT_DIR__ : '',
14
+ typeof __CHRONICLE_PROJECT_ROOT__ !== 'undefined' ? __CHRONICLE_PROJECT_ROOT__ : '',
15
+ ]
16
+ for (const root of roots) {
17
+ if (root && file.startsWith(root)) return file.slice(root.length).replace(/^\//, '')
18
+ }
19
+ return file
20
+ }
21
+
22
+ function frameHtml(frame: CodeFrame): string {
23
+ const gutter = String(frame.lines[frame.lines.length - 1]?.number ?? 0).length
24
+ const rows = frame.lines.flatMap(({ number, text, target }) => {
25
+ const row = `<div class="row${target ? ' target' : ''}"><span class="ln">${target ? '&gt;' : ' '} ${String(number).padStart(gutter)}</span><span class="src">${escapeHtml(text)}</span></div>`
26
+ if (target && frame.caretColumn) {
27
+ return [row, `<div class="row caret"><span class="ln">${' '.repeat(gutter + 1)}</span><span class="src">${' '.repeat(frame.caretColumn - 1)}^</span></div>`]
28
+ }
29
+ return [row]
30
+ })
31
+ return `<pre class="frame">${rows.join('')}</pre>`
32
+ }
33
+
34
+ /**
35
+ * Dev-only: renders a styled error page for MDX failures (syntax errors,
36
+ * unknown components) naming the file, position, and a source code frame.
37
+ * Returns null when the error is not MDX-related.
38
+ */
39
+ export async function renderMdxErrorResponse(err: unknown): Promise<Response | null> {
40
+ const info = parseMdxError(err)
41
+ if (!info) return null
42
+
43
+ let frame: CodeFrame | null = null
44
+ if (info.file && info.line) {
45
+ try {
46
+ const fs = await import('node:fs/promises')
47
+ frame = buildCodeFrame(await fs.readFile(info.file, 'utf-8'), info.line, info.column)
48
+ } catch {
49
+ // source not readable — show the message without a code frame
50
+ }
51
+ }
52
+
53
+ const location = info.file
54
+ ? `${displayPath(info.file)}${info.line ? `:${info.line}${info.column ? `:${info.column}` : ''}` : ''}`
55
+ : null
56
+
57
+ const html = `<!DOCTYPE html>
58
+ <html lang="en">
59
+ <head>
60
+ <meta charset="UTF-8">
61
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
62
+ <title>MDX Error</title>
63
+ <style>
64
+ :root { color-scheme: light dark; }
65
+ body {
66
+ margin: 0; padding: 48px 24px;
67
+ font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
68
+ background: #fff; color: #18181b;
69
+ }
70
+ .card { max-width: 860px; margin: 0 auto; }
71
+ .badge {
72
+ display: inline-block; padding: 4px 10px; border-radius: 6px;
73
+ background: #dc2626; color: #fff; font-size: 12px; font-weight: 600;
74
+ letter-spacing: 0.04em; text-transform: uppercase;
75
+ }
76
+ h1 { font-size: 20px; line-height: 1.5; margin: 16px 0 8px; }
77
+ .loc {
78
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
79
+ font-size: 13px; color: #71717a; margin-bottom: 24px;
80
+ }
81
+ .frame {
82
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
83
+ font-size: 13px; line-height: 1.7; overflow-x: auto;
84
+ background: #fafafa; border: 1px solid #e4e4e7; border-radius: 8px;
85
+ padding: 12px 0; margin: 0;
86
+ }
87
+ .row { display: flex; white-space: pre; }
88
+ .row .ln {
89
+ flex: none; padding: 0 12px; color: #a1a1aa;
90
+ border-right: 1px solid #e4e4e7; margin-right: 12px; user-select: none;
91
+ }
92
+ .row.target { background: rgba(220, 38, 38, 0.08); }
93
+ .row.target .ln, .row.caret .src { color: #dc2626; font-weight: 600; }
94
+ .hint { font-size: 13px; color: #71717a; margin-top: 24px; }
95
+ @media (prefers-color-scheme: dark) {
96
+ body { background: #09090b; color: #f4f4f5; }
97
+ .frame { background: #18181b; border-color: #27272a; }
98
+ .row .ln { border-color: #27272a; color: #52525b; }
99
+ .loc, .hint { color: #a1a1aa; }
100
+ }
101
+ </style>
102
+ </head>
103
+ <body>
104
+ <div class="card">
105
+ <span class="badge">MDX Error</span>
106
+ <h1>${escapeHtml(info.message)}</h1>
107
+ ${location ? `<div class="loc">${escapeHtml(location)}</div>` : ''}
108
+ ${frame ? frameHtml(frame) : ''}
109
+ <p class="hint">Fix the file and save — the page reloads automatically.</p>
110
+ </div>
111
+ <script>
112
+ (function poll(last) {
113
+ setTimeout(async () => {
114
+ try {
115
+ const res = await fetch(location.href, { headers: { accept: 'text/html' } });
116
+ if (res.ok) return location.reload();
117
+ const body = await res.text();
118
+ if (last !== null && body !== last) return location.reload();
119
+ poll(body);
120
+ } catch {
121
+ poll(last);
122
+ }
123
+ }, 1000);
124
+ })(null);
125
+ </script>
126
+ </body>
127
+ </html>`
128
+
129
+ return new Response(html, {
130
+ status: 500,
131
+ headers: { 'Content-Type': 'text/html;charset=utf-8' },
132
+ })
133
+ }
@@ -193,6 +193,7 @@ export default {
193
193
  </body>
194
194
  </html>,
195
195
  );
196
+ await stream.allReady;
196
197
 
197
198
  const renderDuration = performance.now() - renderStart;
198
199
 
@@ -207,6 +208,11 @@ export default {
207
208
  });
208
209
  } catch (err) {
209
210
  console.error(`[chronicle] SSR error for ${pathname}:`, err);
211
+ if (import.meta.env.DEV) {
212
+ const { renderMdxErrorResponse } = await import('./dev-error-page');
213
+ const mdxError = await renderMdxErrorResponse(err);
214
+ if (mdxError) return mdxError;
215
+ }
210
216
  return errorResponse(500, 'Internal Server Error', err instanceof Error ? err.message : String(err));
211
217
  }
212
218
  },
@@ -0,0 +1,151 @@
1
+ import '@vitejs/plugin-react/preamble';
2
+ import React from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { BrowserRouter } from 'react-router';
5
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
6
+ import { QueryClientProvider } from '@tanstack/react-query';
7
+ import { mdxComponents } from '@/components/mdx';
8
+ import { getApiConfigsForVersion } from '@/lib/config';
9
+ import { PageProvider } from '@/lib/page-context';
10
+ import { queryClient } from '@/lib/preload';
11
+ import { resolveRoute, resolveContentRootRedirect, RouteType } from '@/lib/route-resolver';
12
+ import { pageDataUrl, specsUrl } from '@/lib/data-urls';
13
+ import { resolveVersionFromUrl, type VersionContext } from '@/lib/version-source';
14
+ import type { ChronicleConfig, Frontmatter, PageNavLink, Root, TableOfContents } from '@/types';
15
+ import type { ApiSpec } from '@/lib/openapi';
16
+ import type { ReactNode } from 'react';
17
+ import { App } from './App';
18
+
19
+ interface StaticEmbeddedData {
20
+ config: ChronicleConfig;
21
+ tree: Root;
22
+ version: VersionContext;
23
+ }
24
+
25
+ const defaultConfig: ChronicleConfig = {
26
+ site: { title: 'Documentation' },
27
+ content: [{ dir: 'docs', label: 'Docs' }],
28
+ };
29
+
30
+ const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
31
+ '../../.content/**/*.{mdx,md}'
32
+ );
33
+
34
+ async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
35
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
36
+ const key = relativePath.endsWith('.md')
37
+ ? `../../.content/${withoutExt}.md`
38
+ : `../../.content/${withoutExt}.mdx`;
39
+ const loader = contentModules[key];
40
+ if (!loader) return { content: null, toc: [] };
41
+ const mod = await loader();
42
+ const content = mod.default
43
+ ? React.createElement(mod.default, { components: mdxComponents })
44
+ : null;
45
+ return { content, toc: mod.toc ?? [] };
46
+ }
47
+
48
+ async function fetchStaticPageData(slug: string[]) {
49
+ const res = await fetch(pageDataUrl(slug));
50
+ if (!res.ok) throw new Error(String(res.status));
51
+ const ct = res.headers.get('content-type') ?? '';
52
+ if (!ct.includes('json')) throw new Error('404');
53
+ return res.json();
54
+ }
55
+
56
+ async function fetchStaticApiSpecs(version: VersionContext): Promise<ApiSpec[]> {
57
+ try {
58
+ const res = await fetch(specsUrl(version.dir));
59
+ if (!res.ok) return [];
60
+ const ct = res.headers.get('content-type') ?? '';
61
+ if (!ct.includes('json')) return [];
62
+ return res.json();
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ async function mount() {
69
+ try {
70
+ const embedded = (
71
+ window as unknown as { __PAGE_DATA__?: StaticEmbeddedData }
72
+ ).__PAGE_DATA__;
73
+
74
+ const config: ChronicleConfig = embedded?.config ?? defaultConfig;
75
+ const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
76
+
77
+ const route = resolveRoute(window.location.pathname, config);
78
+
79
+ if (route.type === RouteType.Redirect) {
80
+ window.location.replace(route.to);
81
+ return;
82
+ }
83
+
84
+ if (route.type === RouteType.DocsPage) {
85
+ const redirect = resolveContentRootRedirect(route.slug, config);
86
+ if (redirect) {
87
+ window.location.replace(redirect);
88
+ return;
89
+ }
90
+ }
91
+
92
+ const routeVersion: VersionContext = resolveVersionFromUrl(
93
+ window.location.pathname,
94
+ config,
95
+ );
96
+ const version: VersionContext = embedded?.version ?? routeVersion;
97
+
98
+ const isApiRoute =
99
+ route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage;
100
+ const apiConfigs = isApiRoute
101
+ ? getApiConfigsForVersion(config, routeVersion.dir)
102
+ : [];
103
+ const apiSpecs: ApiSpec[] = apiConfigs.length
104
+ ? await fetchStaticApiSpecs(routeVersion)
105
+ : [];
106
+
107
+ let page = null;
108
+ if (route.type === RouteType.DocsPage) {
109
+ try {
110
+ const data = await fetchStaticPageData(route.slug);
111
+ const mdxPath = data.originalPath || data.relativePath;
112
+ if (mdxPath && data.frontmatter) {
113
+ const { content, toc } = await loadMdxModule(mdxPath);
114
+ page = {
115
+ slug: route.slug,
116
+ frontmatter: data.frontmatter as Frontmatter,
117
+ prev: (data.prev as PageNavLink) ?? null,
118
+ next: (data.next as PageNavLink) ?? null,
119
+ content,
120
+ toc,
121
+ };
122
+ }
123
+ } catch {
124
+ // page will remain null, context will show 404
125
+ }
126
+ }
127
+
128
+ createRoot(document.getElementById('root')!).render(
129
+ <QueryClientProvider client={queryClient}>
130
+ <BrowserRouter>
131
+ <ReactRouterProvider>
132
+ <PageProvider
133
+ initialConfig={config}
134
+ initialTree={tree}
135
+ initialPage={page}
136
+ initialApiSpecs={apiSpecs}
137
+ initialVersion={version}
138
+ loadMdx={loadMdxModule}
139
+ >
140
+ <App />
141
+ </PageProvider>
142
+ </ReactRouterProvider>
143
+ </BrowserRouter>
144
+ </QueryClientProvider>
145
+ );
146
+ } catch (err) {
147
+ console.error('Static mount failed:', err);
148
+ }
149
+ }
150
+
151
+ mount();
@@ -1,9 +1,21 @@
1
1
  import { defineErrorHandler, HTTPError } from 'nitro';
2
2
 
3
- export default defineErrorHandler((error, _event) => {
3
+ export default defineErrorHandler(async (error, _event) => {
4
4
  const status = HTTPError.isError(error) ? error.status : 500;
5
5
  const message = error.message || 'Internal Server Error';
6
6
 
7
+ // MDX failures in content (syntax errors, unknown components) surface here:
8
+ // the eager frontmatter glob in source.ts compiles every content file at
9
+ // module load, before the SSR fetch handler can catch anything.
10
+ if (import.meta.env.DEV) {
11
+ const { renderMdxErrorResponse } = await import('./dev-error-page');
12
+ const candidates = [error, (error as { cause?: unknown }).cause];
13
+ for (const candidate of candidates) {
14
+ const response = candidate ? await renderMdxErrorResponse(candidate) : null;
15
+ if (response) return response;
16
+ }
17
+ }
18
+
7
19
  return new Response(JSON.stringify({ error: true, status, message }), {
8
20
  status,
9
21
  headers: { 'Content-Type': 'application/json' },
@@ -82,7 +82,8 @@ export default definePlugin((nitroApp) => {
82
82
  })
83
83
 
84
84
  nitroApp.hooks.hook('chronicle:ssr-rendered', (route, status, durationMs) => {
85
- ssrRenderDuration.record(durationMs, { route: toEndpoint(route), status })
85
+ const endpoint = status === 404 ? '/api/not-found' : toEndpoint(route)
86
+ ssrRenderDuration.record(durationMs, { route: endpoint, status })
86
87
  })
87
88
 
88
89
  nitroApp.hooks.hook('request', (event) => {
@@ -95,7 +96,7 @@ export default definePlugin((nitroApp) => {
95
96
  const duration = performance.now() - start
96
97
  const method = event.req.method
97
98
  const route = new URL(event.req.url).pathname
98
- const endpoint = toEndpoint(route)
99
+ const endpoint = res.status === 404 ? '/api/not-found' : toEndpoint(route)
99
100
 
100
101
  const clientIp =
101
102
  event.req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ??
@@ -11,6 +11,7 @@ import remarkResolveImages from '../lib/remark-resolve-images';
11
11
  import remarkResolveLinks from '../lib/remark-resolve-links';
12
12
  import remarkReadingTime from 'remark-reading-time';
13
13
  import remarkUnusedDirectives from '../lib/remark-unused-directives';
14
+ import remarkValidateMdx from '../lib/remark-validate-mdx';
14
15
 
15
16
  function getDatabaseConnector(preset?: string): { connector: string; options?: Record<string, unknown> } {
16
17
  switch (preset) {
@@ -27,7 +28,7 @@ function getDatabaseConnector(preset?: string): { connector: string; options?: R
27
28
 
28
29
  const STATIC_PRESETS = new Set(['static', 'vercel-static', 'cloudflare-pages', 'github-pages']);
29
30
 
30
- function isStaticPreset(preset?: string): boolean {
31
+ export function isStaticPreset(preset?: string): boolean {
31
32
  return !!preset && STATIC_PRESETS.has(preset);
32
33
  }
33
34
 
@@ -72,8 +73,9 @@ export async function createViteConfig(
72
73
  plugins: [
73
74
  nitro({
74
75
  serverDir: path.resolve(packageRoot, 'src/server'),
75
- ...(preset && { preset }),
76
+ ...(!isStaticPreset(preset) && preset && { preset }),
76
77
  ignore: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
78
+ ...(isStaticPreset(preset) && { prerender: { routes: [] } }),
77
79
  }),
78
80
  mdx({
79
81
  default: defineFumadocsConfig({
@@ -104,6 +106,7 @@ export async function createViteConfig(
104
106
  [remarkResolveImages, { optimize: !isStaticPreset(preset) }],
105
107
  remarkMdxMermaid,
106
108
  remarkReadingTime,
109
+ remarkValidateMdx,
107
110
  ],
108
111
  },
109
112
  }),
@@ -146,8 +149,14 @@ export async function createViteConfig(
146
149
  environments: {
147
150
  client: {
148
151
  build: {
152
+ manifest: isStaticPreset(preset),
149
153
  rollupOptions: {
150
- input: path.resolve(packageRoot, 'src/server/entry-client.tsx')
154
+ input: path.resolve(
155
+ packageRoot,
156
+ isStaticPreset(preset)
157
+ ? 'src/server/entry-static.tsx'
158
+ : 'src/server/entry-client.tsx',
159
+ )
151
160
  }
152
161
  }
153
162
  }
@@ -166,12 +175,16 @@ export async function createViteConfig(
166
175
  base: path.resolve(projectRoot, '.cache/images'),
167
176
  },
168
177
  },
169
- experimental: {
170
- database: true,
171
- },
172
- database: {
173
- default: getDatabaseConnector(preset),
174
- },
178
+ ...(isStaticPreset(preset)
179
+ ? {}
180
+ : {
181
+ experimental: {
182
+ database: true,
183
+ },
184
+ database: {
185
+ default: getDatabaseConnector(preset),
186
+ },
187
+ }),
175
188
  },
176
189
  };
177
190
  }
@@ -126,6 +126,8 @@
126
126
  table-layout: fixed;
127
127
  width: 100%;
128
128
  margin-bottom: var(--rs-space-5);
129
+ /* block inherited justify (e.g. from <p align="justify"> in content) without overriding GFM column alignment */
130
+ text-align: start;
129
131
  }
130
132
 
131
133
  .content table td,
@@ -84,7 +84,3 @@
84
84
  .contentFull {
85
85
  margin-left: 0;
86
86
  }
87
-
88
- .hiddenTrigger {
89
- display: none;
90
- }
@@ -6,7 +6,6 @@ import { useLocation, useNavigate } from 'react-router';
6
6
  import { getLandingEntries } from '@/lib/config';
7
7
  import { getActiveContentDir } from '@/lib/navigation';
8
8
  import { usePageContext } from '@/lib/page-context';
9
- import { Search } from '@/components/ui/search';
10
9
  import type { ThemeLayoutProps } from '@/types';
11
10
  import { ChapterNav } from './ChapterNav';
12
11
  import styles from './Layout.module.css';
@@ -83,7 +82,6 @@ function LayoutInner({
83
82
  </aside>
84
83
  ) : null}
85
84
  <div className={cx(styles.content, classNames?.content, { [styles.contentFull]: !showSidebar })}>
86
- {config.search?.enabled && <Search classNames={{ trigger: styles.hiddenTrigger }} />}
87
85
  {children}
88
86
  </div>
89
87
  </Flex>
@@ -204,6 +204,8 @@
204
204
  table-layout: fixed;
205
205
  width: 100%;
206
206
  margin-bottom: var(--rs-space-5);
207
+ /* block inherited justify (e.g. from <p align="justify"> in content) without overriding GFM column alignment */
208
+ text-align: start;
207
209
  }
208
210
 
209
211
  .content table td,